thumbgate 0.9.10

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.
Files changed (364) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1484 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +283 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1014 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +299 -0
  117. package/scripts/auto-wire-hooks.js +312 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +97 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +263 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +209 -0
  152. package/scripts/ephemeral-agent-store.js +219 -0
  153. package/scripts/eval-harness.js +56 -0
  154. package/scripts/evolution-state.js +241 -0
  155. package/scripts/experiment-tracker.js +267 -0
  156. package/scripts/export-databricks-bundle.js +242 -0
  157. package/scripts/export-dpo-pairs.js +345 -0
  158. package/scripts/export-kto-pairs.js +310 -0
  159. package/scripts/export-training.js +448 -0
  160. package/scripts/failure-diagnostics.js +558 -0
  161. package/scripts/feedback-attribution.js +313 -0
  162. package/scripts/feedback-fallback.js +111 -0
  163. package/scripts/feedback-history-distiller.js +391 -0
  164. package/scripts/feedback-inbox-read.js +162 -0
  165. package/scripts/feedback-loop.js +1887 -0
  166. package/scripts/feedback-paths.js +145 -0
  167. package/scripts/feedback-quality.js +139 -0
  168. package/scripts/feedback-root-consolidator.js +238 -0
  169. package/scripts/feedback-schema.js +426 -0
  170. package/scripts/feedback-session.js +286 -0
  171. package/scripts/feedback-to-memory.js +185 -0
  172. package/scripts/feedback-to-rules.js +163 -0
  173. package/scripts/filesystem-search.js +404 -0
  174. package/scripts/funnel-analytics.js +35 -0
  175. package/scripts/gate-satisfy.js +42 -0
  176. package/scripts/gate-stats.js +116 -0
  177. package/scripts/gate-templates.js +70 -0
  178. package/scripts/gates-engine.js +816 -0
  179. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  180. package/scripts/generate-pretool-hook.sh +40 -0
  181. package/scripts/github-about.js +350 -0
  182. package/scripts/github-outreach.js +65 -0
  183. package/scripts/gtm-revenue-loop.js +520 -0
  184. package/scripts/hallucination-detector.js +226 -0
  185. package/scripts/hf-papers.js +317 -0
  186. package/scripts/history-distiller.js +200 -0
  187. package/scripts/hook-auto-capture.sh +95 -0
  188. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  189. package/scripts/hook-stop-self-score.sh +51 -0
  190. package/scripts/hook-stop-verify-deploy.sh +31 -0
  191. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  192. package/scripts/hook-verify-before-done.sh +20 -0
  193. package/scripts/hosted-config.js +170 -0
  194. package/scripts/hybrid-feedback-context.js +676 -0
  195. package/scripts/install-mcp.js +159 -0
  196. package/scripts/intent-router.js +392 -0
  197. package/scripts/internal-agent-bootstrap.js +490 -0
  198. package/scripts/jsonl-watcher.js +155 -0
  199. package/scripts/lesson-db.js +613 -0
  200. package/scripts/lesson-inference.js +315 -0
  201. package/scripts/lesson-retrieval.js +95 -0
  202. package/scripts/lesson-rotation.js +137 -0
  203. package/scripts/lesson-search.js +644 -0
  204. package/scripts/lesson-synthesis.js +196 -0
  205. package/scripts/license.js +50 -0
  206. package/scripts/local-model-profile.js +383 -0
  207. package/scripts/markdown-escape.js +12 -0
  208. package/scripts/marketing-experiment.js +671 -0
  209. package/scripts/mcp-config.js +149 -0
  210. package/scripts/mcp-policy.js +99 -0
  211. package/scripts/memalign-recall.js +111 -0
  212. package/scripts/memory-firewall.js +222 -0
  213. package/scripts/memory-migration.js +296 -0
  214. package/scripts/meta-policy.js +194 -0
  215. package/scripts/metered-billing.js +16 -0
  216. package/scripts/model-tier-router.js +301 -0
  217. package/scripts/money-watcher.js +71 -0
  218. package/scripts/multi-hop-recall.js +240 -0
  219. package/scripts/natural-language-harness.js +330 -0
  220. package/scripts/obsidian-export.js +712 -0
  221. package/scripts/operational-dashboard.js +103 -0
  222. package/scripts/operational-summary.js +93 -0
  223. package/scripts/optimize-context.js +17 -0
  224. package/scripts/org-dashboard.js +201 -0
  225. package/scripts/partner-orchestration.js +146 -0
  226. package/scripts/per-step-scoring.js +165 -0
  227. package/scripts/perplexity-marketing.js +466 -0
  228. package/scripts/pii-scanner.js +153 -0
  229. package/scripts/plan-gate.js +154 -0
  230. package/scripts/post-everywhere.js +308 -0
  231. package/scripts/post-to-x-retry.sh +22 -0
  232. package/scripts/post-to-x.js +369 -0
  233. package/scripts/pr-manager.js +236 -0
  234. package/scripts/predictive-insights.js +356 -0
  235. package/scripts/principle-extractor.js +162 -0
  236. package/scripts/pro-features.js +40 -0
  237. package/scripts/pro-local-dashboard.js +174 -0
  238. package/scripts/problem-detail.js +53 -0
  239. package/scripts/product-feedback.js +134 -0
  240. package/scripts/profile-router.js +245 -0
  241. package/scripts/prompt-dlp.js +221 -0
  242. package/scripts/prompt-guard.js +83 -0
  243. package/scripts/prove-adapters.js +863 -0
  244. package/scripts/prove-attribution.js +365 -0
  245. package/scripts/prove-automation.js +653 -0
  246. package/scripts/prove-autoresearch.js +304 -0
  247. package/scripts/prove-claim-verification.js +277 -0
  248. package/scripts/prove-cloudflare-sandbox.js +163 -0
  249. package/scripts/prove-data-pipeline.js +410 -0
  250. package/scripts/prove-data-quality.js +227 -0
  251. package/scripts/prove-evolution.js +352 -0
  252. package/scripts/prove-harnesses.js +287 -0
  253. package/scripts/prove-intelligence.js +259 -0
  254. package/scripts/prove-lancedb.js +371 -0
  255. package/scripts/prove-local-intelligence.js +342 -0
  256. package/scripts/prove-loop-closure.js +263 -0
  257. package/scripts/prove-predictive-insights.js +357 -0
  258. package/scripts/prove-runtime.js +350 -0
  259. package/scripts/prove-seo-gsd.js +234 -0
  260. package/scripts/prove-settings.js +279 -0
  261. package/scripts/prove-subway-upgrades.js +277 -0
  262. package/scripts/prove-tessl.js +229 -0
  263. package/scripts/prove-training-export.js +327 -0
  264. package/scripts/prove-workflow-contract.js +116 -0
  265. package/scripts/prove-xmemory.js +332 -0
  266. package/scripts/publish-decision.js +133 -0
  267. package/scripts/pulse.js +80 -0
  268. package/scripts/rate-limiter.js +125 -0
  269. package/scripts/reddit-dm-outreach.js +182 -0
  270. package/scripts/reddit-monitor-cron.sh +26 -0
  271. package/scripts/reflector-agent.js +221 -0
  272. package/scripts/reminder-engine.js +132 -0
  273. package/scripts/revenue-status.js +472 -0
  274. package/scripts/risk-scorer.js +458 -0
  275. package/scripts/rlaif-self-audit.js +129 -0
  276. package/scripts/rubric-engine.js +230 -0
  277. package/scripts/schedule-manager.js +251 -0
  278. package/scripts/secret-scanner.js +414 -0
  279. package/scripts/self-heal.js +147 -0
  280. package/scripts/self-healing-check.js +188 -0
  281. package/scripts/semantic-layer.js +98 -0
  282. package/scripts/seo-gsd.js +1153 -0
  283. package/scripts/settings-hierarchy.js +214 -0
  284. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  285. package/scripts/skill-exporter.js +262 -0
  286. package/scripts/skill-generator.js +446 -0
  287. package/scripts/skill-materializer.js +134 -0
  288. package/scripts/skill-packs.js +136 -0
  289. package/scripts/skill-proposer.js +99 -0
  290. package/scripts/skill-quality-tracker.js +284 -0
  291. package/scripts/slo-alert-engine.js +14 -0
  292. package/scripts/slow-loop.js +72 -0
  293. package/scripts/social-analytics/db/schema.sql +32 -0
  294. package/scripts/social-analytics/digest.js +256 -0
  295. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  296. package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
  297. package/scripts/social-analytics/mcp-server.js +289 -0
  298. package/scripts/social-analytics/normalizer.js +580 -0
  299. package/scripts/social-analytics/notify.js +162 -0
  300. package/scripts/social-analytics/poll-all.js +107 -0
  301. package/scripts/social-analytics/pollers/github.js +195 -0
  302. package/scripts/social-analytics/pollers/instagram.js +253 -0
  303. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  304. package/scripts/social-analytics/pollers/plausible.js +247 -0
  305. package/scripts/social-analytics/pollers/reddit.js +306 -0
  306. package/scripts/social-analytics/pollers/threads.js +233 -0
  307. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  308. package/scripts/social-analytics/pollers/x.js +227 -0
  309. package/scripts/social-analytics/pollers/youtube.js +304 -0
  310. package/scripts/social-analytics/pollers/zernio.js +180 -0
  311. package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
  312. package/scripts/social-analytics/publishers/devto.js +122 -0
  313. package/scripts/social-analytics/publishers/instagram.js +317 -0
  314. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  315. package/scripts/social-analytics/publishers/reddit.js +390 -0
  316. package/scripts/social-analytics/publishers/threads.js +275 -0
  317. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  318. package/scripts/social-analytics/publishers/x.js +259 -0
  319. package/scripts/social-analytics/publishers/youtube.js +223 -0
  320. package/scripts/social-analytics/publishers/zernio.js +209 -0
  321. package/scripts/social-analytics/run-digest.js +34 -0
  322. package/scripts/social-analytics/store.js +257 -0
  323. package/scripts/social-analytics/utm.js +143 -0
  324. package/scripts/social-pipeline.js +2628 -0
  325. package/scripts/social-quality-gate.js +18 -0
  326. package/scripts/social-reply-monitor.js +445 -0
  327. package/scripts/status-dashboard.js +155 -0
  328. package/scripts/statusline-lesson.js +16 -0
  329. package/scripts/statusline-tower.js +8 -0
  330. package/scripts/statusline.sh +116 -0
  331. package/scripts/stripe-live-status.js +115 -0
  332. package/scripts/subagent-profiles.js +79 -0
  333. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  334. package/scripts/sync-github-about.js +52 -0
  335. package/scripts/sync-version.js +451 -0
  336. package/scripts/synthetic-dpo.js +234 -0
  337. package/scripts/telemetry-analytics.js +821 -0
  338. package/scripts/tessl-export.js +371 -0
  339. package/scripts/test-coverage.js +120 -0
  340. package/scripts/thompson-sampling.js +417 -0
  341. package/scripts/thumbgate-search.js +189 -0
  342. package/scripts/tool-kpi-tracker.js +12 -0
  343. package/scripts/tool-registry.js +811 -0
  344. package/scripts/train_from_feedback.py +910 -0
  345. package/scripts/user-profile.js +78 -0
  346. package/scripts/validate-feedback.js +580 -0
  347. package/scripts/validate-workflow-contract.js +287 -0
  348. package/scripts/vector-store.js +198 -0
  349. package/scripts/verification-loop.js +291 -0
  350. package/scripts/verify-obsidian-setup.sh +269 -0
  351. package/scripts/verify-run.js +269 -0
  352. package/scripts/webhook-delivery.js +62 -0
  353. package/scripts/weekly-auto-post.js +124 -0
  354. package/scripts/workflow-runs.js +154 -0
  355. package/scripts/workflow-sprint-intake.js +475 -0
  356. package/scripts/workspace-evolver.js +374 -0
  357. package/scripts/x-autonomous-marketing.js +139 -0
  358. package/scripts/xmemory-lite.js +405 -0
  359. package/skills/agent-memory/SKILL.md +97 -0
  360. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  361. package/skills/solve-architecture-autonomy/tool.js +33 -0
  362. package/skills/thumbgate/SKILL.md +114 -0
  363. package/skills/thumbgate-feedback/SKILL.md +49 -0
  364. package/src/api/server.js +4208 -0
@@ -0,0 +1,2438 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * billing.js — Stripe billing integration using official Stripe SDK.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const STRIPE_TIMEOUT_MS = 5000;
9
+ function withTimeout(promise, ms = STRIPE_TIMEOUT_MS) {
10
+ return Promise.race([
11
+ promise,
12
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Stripe API timeout after ${ms}ms`)), ms)),
13
+ ]);
14
+ }
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const crypto = require('crypto');
19
+ const Stripe = require('stripe');
20
+ const { createTraceId } = require('./hosted-config');
21
+ const {
22
+ getFeedbackPaths,
23
+ getLegacyFeedbackDir,
24
+ getFallbackFeedbackDir,
25
+ resolveFallbackArtifactPath,
26
+ } = require('./feedback-paths');
27
+ const { getTelemetryAnalytics, getTelemetrySourceDiagnostics } = require('./telemetry-analytics');
28
+ const { loadWorkflowSprintLeads } = require('./workflow-sprint-intake');
29
+ const {
30
+ PRO_MONTHLY_PRICE_ID,
31
+ PRO_ANNUAL_PRICE_ID,
32
+ TEAM_MONTHLY_PRICE_ID,
33
+ PRO_MONTHLY_PRICE_DOLLARS,
34
+ PRO_ANNUAL_PRICE_DOLLARS,
35
+ TEAM_MONTHLY_PRICE_DOLLARS,
36
+ TEAM_MIN_SEATS,
37
+ normalizePlanId,
38
+ normalizeBillingCycle,
39
+ normalizeSeatCount,
40
+ } = require('./commercial-offer');
41
+ const {
42
+ eventOccursInWindow,
43
+ filterEntriesForWindow,
44
+ resolveAnalyticsWindow,
45
+ serializeAnalyticsWindow,
46
+ } = require('./analytics-window');
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Config
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const CONFIG = {
53
+ STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY || '',
54
+ STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET || '',
55
+ GITHUB_MARKETPLACE_WEBHOOK_SECRET: process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '',
56
+ GITHUB_MARKETPLACE_PLAN_PRICES_JSON: process.env.THUMBGATE_GITHUB_MARKETPLACE_PLAN_PRICES_JSON || '',
57
+ STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID || PRO_MONTHLY_PRICE_ID,
58
+ STRIPE_PRICE_ID_PRO_MONTHLY: process.env.STRIPE_PRICE_ID_PRO_MONTHLY || PRO_MONTHLY_PRICE_ID,
59
+ STRIPE_PRICE_ID_PRO_ANNUAL: process.env.STRIPE_PRICE_ID_PRO_ANNUAL || PRO_ANNUAL_PRICE_ID,
60
+ STRIPE_PRICE_ID_TEAM_MONTHLY: process.env.STRIPE_PRICE_ID_TEAM_MONTHLY || TEAM_MONTHLY_PRICE_ID,
61
+ STRIPE_PRODUCT_ID: process.env.STRIPE_PRODUCT_ID || '',
62
+ get API_KEYS_PATH() {
63
+ return process.env._TEST_API_KEYS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'api-keys.json');
64
+ },
65
+ get FUNNEL_LEDGER_PATH() {
66
+ return process.env._TEST_FUNNEL_LEDGER_PATH || process.env.THUMBGATE_FUNNEL_LEDGER_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'funnel-events.jsonl');
67
+ },
68
+ get REVENUE_LEDGER_PATH() {
69
+ return process.env._TEST_REVENUE_LEDGER_PATH || process.env.THUMBGATE_REVENUE_LEDGER_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'revenue-events.jsonl');
70
+ },
71
+ get LOCAL_CHECKOUT_SESSIONS_PATH() {
72
+ return process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'local-checkout-sessions.json');
73
+ },
74
+ CREDIT_PACKS: {
75
+ 'mistake-free-starter': {
76
+ id: 'mistake-free-starter',
77
+ name: 'Mistake-Free Starter Pack',
78
+ amountCents: 4900,
79
+ credits: 500,
80
+ currency: 'USD',
81
+ }
82
+ }
83
+ };
84
+
85
+ function resolveLegacyBillingPath(fileName) {
86
+ return resolveFallbackArtifactPath(fileName, {
87
+ feedbackDir: getFeedbackPaths().FEEDBACK_DIR,
88
+ });
89
+ }
90
+
91
+ let _stripeClient = null;
92
+ function getStripeClient() {
93
+ if (!_stripeClient) {
94
+ if (!CONFIG.STRIPE_SECRET_KEY) {
95
+ throw new Error('STRIPE_SECRET_KEY is missing. Stripe client cannot be initialized.');
96
+ }
97
+ _stripeClient = new Stripe(CONFIG.STRIPE_SECRET_KEY);
98
+ }
99
+ return _stripeClient;
100
+ }
101
+
102
+ const LOCAL_MODE = () => !CONFIG.STRIPE_SECRET_KEY;
103
+ const IS_TEST = !!(
104
+ process.env._TEST_API_KEYS_PATH ||
105
+ process.env._TEST_FUNNEL_LEDGER_PATH ||
106
+ process.env._TEST_REVENUE_LEDGER_PATH ||
107
+ process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH ||
108
+ process.env.NODE_ENV === 'test'
109
+ );
110
+
111
+ function shouldMergeLegacyBillingData() {
112
+ return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
113
+ || process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
114
+ }
115
+
116
+ function safeCompareHex(expectedHex, actualHex) {
117
+ try {
118
+ const expected = Buffer.from(expectedHex, 'hex');
119
+ const actual = Buffer.from(actualHex, 'hex');
120
+ if (expected.length === 0 || expected.length !== actual.length) {
121
+ return false;
122
+ }
123
+ return crypto.timingSafeEqual(expected, actual);
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Internal helpers
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function ensureParentDir(filePath) {
134
+ const dir = path.dirname(filePath);
135
+ if (!fs.existsSync(dir)) {
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ }
138
+ }
139
+
140
+ function sanitizeMetadata(metadata) {
141
+ if (!metadata || typeof metadata !== 'object') return {};
142
+ try {
143
+ return JSON.parse(JSON.stringify(metadata));
144
+ } catch {
145
+ return { ...metadata };
146
+ }
147
+ }
148
+
149
+ function appendJsonlRecord(filePath, payload) {
150
+ try {
151
+ ensureParentDir(filePath);
152
+ fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, 'utf-8');
153
+ return { written: true, payload };
154
+ } catch (err) {
155
+ return { written: false, reason: 'write_failed', error: err.message };
156
+ }
157
+ }
158
+
159
+ function loadJsonlRecords(filePath, legacyPath = null) {
160
+ try {
161
+ const paths = [];
162
+ const primaryExists = Boolean(filePath && fs.existsSync(filePath));
163
+ const legacyExists = Boolean(legacyPath && legacyPath !== filePath && fs.existsSync(legacyPath));
164
+
165
+ if (primaryExists) {
166
+ paths.push(filePath);
167
+ if (legacyExists && shouldMergeLegacyBillingData()) {
168
+ paths.push(legacyPath);
169
+ }
170
+ } else if (legacyExists) {
171
+ paths.push(legacyPath);
172
+ }
173
+
174
+ const merged = [];
175
+ const seen = new Set();
176
+
177
+ for (const target of paths) {
178
+ if (!fs.existsSync(target)) continue;
179
+ const rows = fs.readFileSync(target, 'utf-8')
180
+ .split('\n')
181
+ .map((line) => line.trim())
182
+ .filter(Boolean)
183
+ .map((line) => {
184
+ try {
185
+ return JSON.parse(line);
186
+ } catch {
187
+ return null;
188
+ }
189
+ })
190
+ .filter(Boolean);
191
+
192
+ for (const row of rows) {
193
+ const key = JSON.stringify(row);
194
+ if (seen.has(key)) continue;
195
+ seen.add(key);
196
+ merged.push(row);
197
+ }
198
+ }
199
+
200
+ return merged;
201
+ } catch { return []; }
202
+ }
203
+
204
+ function loadJsonlFile(filePath) {
205
+ try {
206
+ if (!fs.existsSync(filePath)) return [];
207
+ return fs.readFileSync(filePath, 'utf-8')
208
+ .split('\n')
209
+ .map((line) => line.trim())
210
+ .filter(Boolean)
211
+ .map((line) => {
212
+ try {
213
+ return JSON.parse(line);
214
+ } catch {
215
+ return null;
216
+ }
217
+ })
218
+ .filter(Boolean);
219
+ } catch {
220
+ return [];
221
+ }
222
+ }
223
+
224
+ function writeJsonlRecords(filePath, rows = []) {
225
+ try {
226
+ ensureParentDir(filePath);
227
+ const serialized = rows
228
+ .filter(Boolean)
229
+ .map((row) => JSON.stringify(row))
230
+ .join('\n');
231
+ fs.writeFileSync(filePath, serialized ? `${serialized}\n` : '', 'utf-8');
232
+ return { written: true, rowCount: rows.filter(Boolean).length };
233
+ } catch (err) {
234
+ return { written: false, reason: 'write_failed', error: err.message };
235
+ }
236
+ }
237
+
238
+ function buildSourceWarning(code, message) {
239
+ return { code, message };
240
+ }
241
+
242
+ function describeDataFile({ primaryPath, legacyPath = null, mode = 'fallback' } = {}) {
243
+ const includeLegacy = Boolean(legacyPath);
244
+ const samePath = Boolean(
245
+ includeLegacy &&
246
+ legacyPath &&
247
+ path.resolve(primaryPath || '.') === path.resolve(legacyPath || '__missing__')
248
+ );
249
+ const normalizedLegacyPath = includeLegacy && !samePath ? legacyPath : null;
250
+ const primaryExists = Boolean(primaryPath && fs.existsSync(primaryPath));
251
+ const legacyExists = Boolean(normalizedLegacyPath && fs.existsSync(normalizedLegacyPath));
252
+ const activePaths = [];
253
+ let activeMode = 'missing';
254
+
255
+ if (mode === 'merge' && shouldMergeLegacyBillingData()) {
256
+ if (primaryExists) activePaths.push(primaryPath);
257
+ if (legacyExists) activePaths.push(normalizedLegacyPath);
258
+ if (primaryExists && legacyExists) activeMode = 'merged';
259
+ else if (primaryExists) activeMode = 'primary';
260
+ else if (legacyExists) activeMode = 'legacy_fallback';
261
+ } else {
262
+ const activePath = primaryExists ? primaryPath : (legacyExists ? normalizedLegacyPath : null);
263
+ if (activePath) activePaths.push(activePath);
264
+ if (primaryExists) activeMode = 'primary';
265
+ else if (legacyExists) activeMode = 'legacy_fallback';
266
+ }
267
+
268
+ return {
269
+ primaryPath,
270
+ legacyPath: normalizedLegacyPath,
271
+ primaryExists,
272
+ legacyExists,
273
+ activeMode,
274
+ activePaths,
275
+ mixedRoots: activeMode === 'merged',
276
+ };
277
+ }
278
+
279
+ function buildBillingSourceDiagnostics(feedbackDir) {
280
+ const keyStore = describeDataFile({
281
+ primaryPath: CONFIG.API_KEYS_PATH,
282
+ legacyPath: resolveLegacyBillingPath('api-keys.json'),
283
+ mode: 'fallback',
284
+ });
285
+ const funnelLedger = describeDataFile({
286
+ primaryPath: CONFIG.FUNNEL_LEDGER_PATH,
287
+ legacyPath: resolveLegacyBillingPath('funnel-events.jsonl'),
288
+ mode: 'fallback',
289
+ });
290
+ const revenueLedger = describeDataFile({
291
+ primaryPath: CONFIG.REVENUE_LEDGER_PATH,
292
+ legacyPath: resolveLegacyBillingPath('revenue-events.jsonl'),
293
+ mode: 'fallback',
294
+ });
295
+ const checkoutSessions = describeDataFile({
296
+ primaryPath: CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH,
297
+ legacyPath: resolveLegacyBillingPath('local-checkout-sessions.json'),
298
+ mode: 'fallback',
299
+ });
300
+ const telemetry = getTelemetrySourceDiagnostics(feedbackDir);
301
+ const warnings = [
302
+ ...telemetry.warnings,
303
+ ];
304
+
305
+ if (keyStore.activeMode === 'legacy_fallback') {
306
+ warnings.push(buildSourceWarning(
307
+ 'key_store_legacy_fallback',
308
+ 'API keys are loading from a legacy feedback directory because the active feedback directory has no key store.'
309
+ ));
310
+ } else if (keyStore.activeMode === 'missing') {
311
+ warnings.push(buildSourceWarning(
312
+ 'key_store_missing',
313
+ 'API key state is missing from both the active and legacy feedback directories.'
314
+ ));
315
+ }
316
+
317
+ if (funnelLedger.activeMode === 'legacy_fallback') {
318
+ warnings.push(buildSourceWarning(
319
+ 'funnel_ledger_legacy_fallback',
320
+ 'Funnel events are loading only from a legacy feedback directory.'
321
+ ));
322
+ } else if (funnelLedger.activeMode === 'missing') {
323
+ warnings.push(buildSourceWarning(
324
+ 'funnel_ledger_missing',
325
+ 'Funnel events are missing from both the active and legacy feedback directories.'
326
+ ));
327
+ }
328
+
329
+ if (revenueLedger.activeMode === 'legacy_fallback') {
330
+ warnings.push(buildSourceWarning(
331
+ 'revenue_ledger_legacy_fallback',
332
+ 'Revenue events are loading only from a legacy feedback directory.'
333
+ ));
334
+ } else if (revenueLedger.activeMode === 'missing') {
335
+ warnings.push(buildSourceWarning(
336
+ 'revenue_ledger_missing',
337
+ 'Revenue events are missing from both the active and legacy feedback directories.'
338
+ ));
339
+ }
340
+
341
+ const mixedRoots = [keyStore, funnelLedger, revenueLedger, checkoutSessions, telemetry]
342
+ .some((descriptor) => descriptor.mixedRoots || descriptor.activeMode === 'legacy_fallback');
343
+ if (mixedRoots) {
344
+ warnings.push(buildSourceWarning(
345
+ 'mixed_feedback_roots',
346
+ 'Analytics are mixing active and legacy feedback roots. Consolidate runtime state before claiming full observability.'
347
+ ));
348
+ }
349
+
350
+ return {
351
+ feedbackDir,
352
+ fallbackFeedbackDir: getFallbackFeedbackDir(),
353
+ legacyFeedbackDir: getLegacyFeedbackDir(),
354
+ mixedRoots,
355
+ files: {
356
+ keyStore,
357
+ funnelLedger,
358
+ revenueLedger,
359
+ checkoutSessions,
360
+ telemetry,
361
+ },
362
+ warnings,
363
+ };
364
+ }
365
+
366
+ function normalizeText(value) {
367
+ if (value === undefined || value === null) return null;
368
+ const text = String(value).trim();
369
+ return text || null;
370
+ }
371
+
372
+ function normalizeCurrency(value) {
373
+ const text = normalizeText(value);
374
+ return text ? text.toUpperCase() : null;
375
+ }
376
+
377
+ function normalizeInteger(value) {
378
+ if (value === undefined || value === null) return null;
379
+ if (typeof value === 'string' && value.trim() === '') return null;
380
+ const num = Number(value);
381
+ return Number.isFinite(num) ? Math.trunc(num) : null;
382
+ }
383
+
384
+ function pickFirstText(...values) {
385
+ for (const value of values) {
386
+ const normalized = normalizeText(value);
387
+ if (normalized) return normalized;
388
+ }
389
+ return null;
390
+ }
391
+
392
+ function safeRate(num, den) {
393
+ return den ? Number((num / den).toFixed(4)) : 0;
394
+ }
395
+
396
+ function incrementCounter(target, key, amount = 1) {
397
+ const resolvedKey = normalizeText(key) || 'unknown';
398
+ target[resolvedKey] = (target[resolvedKey] || 0) + amount;
399
+ }
400
+
401
+ function extractAttribution(metadata = {}) {
402
+ const safe = sanitizeMetadata(metadata);
403
+ return {
404
+ source: normalizeText(safe.utmSource || safe.source),
405
+ medium: normalizeText(safe.utmMedium || safe.medium),
406
+ campaign: normalizeText(safe.utmCampaign || safe.campaign),
407
+ content: normalizeText(safe.utmContent || safe.content),
408
+ term: normalizeText(safe.utmTerm || safe.term),
409
+ creator: normalizeText(safe.creator || safe.creatorHandle || safe.creator_handle),
410
+ community: normalizeText(safe.community || safe.subreddit),
411
+ postId: normalizeText(safe.postId || safe.post_id),
412
+ commentId: normalizeText(safe.commentId || safe.comment_id),
413
+ campaignVariant: normalizeText(safe.campaignVariant || safe.variant),
414
+ offerCode: normalizeText(safe.offerCode || safe.offer || safe.coupon),
415
+ referrer: normalizeText(safe.referrer),
416
+ landingPath: normalizeText(safe.landingPath),
417
+ ctaId: normalizeText(safe.ctaId),
418
+ };
419
+ }
420
+
421
+ function extractJourneyFields(metadata = {}) {
422
+ const safe = sanitizeMetadata(metadata);
423
+ const attribution = extractAttribution(safe);
424
+ return {
425
+ acquisitionId: normalizeText(safe.acquisitionId),
426
+ visitorId: normalizeText(safe.visitorId),
427
+ sessionId: normalizeText(safe.sessionId),
428
+ ctaId: attribution.ctaId,
429
+ ctaPlacement: normalizeText(safe.ctaPlacement),
430
+ planId: normalizeText(safe.planId),
431
+ creator: attribution.creator,
432
+ community: attribution.community,
433
+ postId: attribution.postId,
434
+ commentId: attribution.commentId,
435
+ campaignVariant: attribution.campaignVariant,
436
+ offerCode: attribution.offerCode,
437
+ referrer: attribution.referrer,
438
+ referrerHost: normalizeText(safe.referrerHost),
439
+ landingPath: attribution.landingPath,
440
+ utmSource: attribution.source,
441
+ utmMedium: attribution.medium,
442
+ utmCampaign: attribution.campaign,
443
+ utmContent: attribution.content,
444
+ utmTerm: attribution.term,
445
+ };
446
+ }
447
+
448
+ function resolveAttributionSource(attribution, fallback = null) {
449
+ return attribution.source || normalizeText(fallback) || 'unknown';
450
+ }
451
+
452
+ function resolveAttributionCampaign(attribution) {
453
+ return attribution.campaign || 'unassigned';
454
+ }
455
+
456
+ function resolveAcquisitionLeadKey(entry = {}) {
457
+ const metadata = sanitizeMetadata(entry.metadata);
458
+ const attribution = sanitizeMetadata(entry.attribution);
459
+ return pickFirstText(
460
+ entry.acquisitionId,
461
+ metadata.acquisitionId,
462
+ attribution.acquisitionId,
463
+ entry.traceId,
464
+ metadata.traceId,
465
+ attribution.traceId,
466
+ entry.visitorId,
467
+ metadata.visitorId,
468
+ attribution.visitorId,
469
+ entry.sessionId,
470
+ metadata.sessionId,
471
+ attribution.sessionId,
472
+ entry.installId,
473
+ metadata.installId,
474
+ attribution.installId,
475
+ entry.orderId,
476
+ metadata.orderId,
477
+ entry.evidence
478
+ );
479
+ }
480
+
481
+ function buildProviderEventFallbackKey(entry = {}, metadata = {}, attribution = {}) {
482
+ const provider = normalizeText(metadata.provider || entry.provider || attribution.source || entry.source) || 'unknown';
483
+ const customerId = normalizeText(metadata.customerId || entry.customerId);
484
+ const accountId = normalizeText(metadata.accountId || entry.accountId);
485
+ const planId = normalizeText(entry.planId || metadata.planId);
486
+ const timestamp = normalizeText(entry.timestamp);
487
+ const evidence = normalizeText(entry.evidence);
488
+
489
+ if (customerId && timestamp) {
490
+ return [provider, customerId, planId || accountId || evidence || 'event', timestamp].join(':');
491
+ }
492
+
493
+ if (accountId && timestamp) {
494
+ return [provider, accountId, planId || evidence || 'event', timestamp].join(':');
495
+ }
496
+
497
+ if (customerId && evidence) {
498
+ return [provider, customerId, planId || evidence].join(':');
499
+ }
500
+
501
+ return null;
502
+ }
503
+
504
+ function resolveEvidenceOrderKey(entry = {}) {
505
+ const evidence = normalizeText(entry.evidence);
506
+ const eventName = normalizeText(entry.event);
507
+ if (!evidence) return null;
508
+ return evidence !== eventName ? evidence : null;
509
+ }
510
+
511
+ function resolvePaidProviderEventKey(entry = {}) {
512
+ const metadata = sanitizeMetadata(entry.metadata);
513
+ const attribution = extractAttribution({
514
+ ...metadata,
515
+ ...sanitizeMetadata(entry.attribution),
516
+ ...sanitizeMetadata(entry),
517
+ });
518
+ return pickFirstText(
519
+ entry.orderId,
520
+ metadata.orderId,
521
+ metadata.sessionId,
522
+ metadata.marketplaceOrderId,
523
+ resolveEvidenceOrderKey(entry),
524
+ buildProviderEventFallbackKey(entry, metadata, attribution),
525
+ resolveAcquisitionLeadKey(entry)
526
+ );
527
+ }
528
+
529
+ function resolveRevenueEventKey(entry = {}) {
530
+ const metadata = sanitizeMetadata(entry.metadata);
531
+ const attribution = sanitizeMetadata(entry.attribution);
532
+ return pickFirstText(
533
+ entry.orderId,
534
+ metadata.orderId,
535
+ metadata.marketplaceOrderId,
536
+ resolveEvidenceOrderKey(entry),
537
+ buildProviderEventFallbackKey(entry, metadata, attribution),
538
+ entry.evidence,
539
+ entry.traceId,
540
+ metadata.traceId,
541
+ attribution.traceId,
542
+ entry.installId,
543
+ metadata.installId,
544
+ attribution.installId,
545
+ entry.customerId
546
+ );
547
+ }
548
+
549
+ function isQualifiedWorkflowSprintLead(entry = {}) {
550
+ return Boolean(
551
+ normalizeText(entry.contact && entry.contact.email) &&
552
+ normalizeText(entry.qualification && entry.qualification.workflow) &&
553
+ normalizeText(entry.qualification && entry.qualification.owner) &&
554
+ normalizeText(entry.qualification && entry.qualification.blocker) &&
555
+ normalizeText(entry.qualification && entry.qualification.runtime)
556
+ );
557
+ }
558
+
559
+ function isOperatorGeneratedAcquisitionEntry(entry = {}) {
560
+ const metadata = sanitizeMetadata(entry.metadata);
561
+ const attribution = extractAttribution({
562
+ ...metadata,
563
+ ...sanitizeMetadata(entry.attribution),
564
+ ...sanitizeMetadata(entry),
565
+ });
566
+ const source = normalizeText(attribution.source || metadata.source || entry.source);
567
+ const medium = normalizeText(attribution.medium || metadata.medium || entry.utmMedium);
568
+ const eventName = normalizeText(entry.event);
569
+
570
+ return source === 'cli' ||
571
+ medium === 'operator_outreach' ||
572
+ eventName === 'outreach_target_generated' ||
573
+ eventName === 'outreach_sequence_started' ||
574
+ eventName === 'lead_list_generated';
575
+ }
576
+
577
+ function hasRevenueEventMatch(entries, target) {
578
+ const targetKey = resolveRevenueEventKey(target);
579
+ if (!targetKey) return false;
580
+ return entries.some((entry) => {
581
+ return normalizeText(entry.status) === normalizeText(target.status) &&
582
+ resolveRevenueEventKey(entry) === targetKey;
583
+ });
584
+ }
585
+
586
+ function hasFunnelEventMatch(entries, target) {
587
+ const targetKey = resolvePaidProviderEventKey(target);
588
+ if (!targetKey) return false;
589
+ return entries.some((entry) => {
590
+ return normalizeText(entry.stage) === normalizeText(target.stage) &&
591
+ normalizeText(entry.event) === normalizeText(target.event) &&
592
+ resolvePaidProviderEventKey(entry) === targetKey;
593
+ });
594
+ }
595
+
596
+ function appendFunnelEvent({ stage, event, installId = null, traceId = null, evidence, metadata = {} } = {}) {
597
+ if (!stage || !event) return { written: false, reason: 'missing_stage_or_event' };
598
+ const payload = {
599
+ timestamp: new Date().toISOString(),
600
+ stage,
601
+ event,
602
+ evidence: evidence || event,
603
+ installId: installId || null,
604
+ traceId: traceId || metadata.traceId || null,
605
+ ...extractJourneyFields(metadata),
606
+ metadata: sanitizeMetadata(metadata),
607
+ };
608
+ return appendJsonlRecord(CONFIG.FUNNEL_LEDGER_PATH, payload);
609
+ }
610
+
611
+ function loadFunnelLedger() {
612
+ return loadJsonlRecords(
613
+ CONFIG.FUNNEL_LEDGER_PATH,
614
+ resolveLegacyBillingPath('funnel-events.jsonl')
615
+ );
616
+ }
617
+
618
+ function loadRevenueLedger() {
619
+ return loadJsonlRecords(
620
+ CONFIG.REVENUE_LEDGER_PATH,
621
+ resolveLegacyBillingPath('revenue-events.jsonl')
622
+ );
623
+ }
624
+
625
+ function resolveRevenueLedgerFilePath() {
626
+ const primary = CONFIG.REVENUE_LEDGER_PATH;
627
+ const legacy = resolveLegacyBillingPath('revenue-events.jsonl');
628
+ if (fs.existsSync(primary) || IS_TEST) {
629
+ return primary;
630
+ }
631
+ if (legacy !== primary && fs.existsSync(legacy)) {
632
+ return legacy;
633
+ }
634
+ return primary;
635
+ }
636
+
637
+ function deriveRevenueEventFromPaidProviderEvent(entry = {}) {
638
+ const metadata = sanitizeMetadata(entry.metadata);
639
+ const provider = normalizeText(metadata.provider || entry.provider || entry.utmSource || entry.source);
640
+ const customerId = normalizeText(metadata.customerId || entry.customerId);
641
+ const orderId = resolvePaidProviderEventKey(entry);
642
+ if (!provider || !customerId || !orderId) return null;
643
+
644
+ const attribution = extractAttribution({
645
+ ...metadata,
646
+ ...sanitizeMetadata(entry.attribution),
647
+ ...sanitizeMetadata(entry),
648
+ });
649
+
650
+ return {
651
+ timestamp: normalizeText(entry.timestamp) || new Date().toISOString(),
652
+ provider,
653
+ event: normalizeText(entry.event) || 'paid_provider_event',
654
+ status: 'paid',
655
+ orderId,
656
+ evidence: normalizeText(entry.evidence) || orderId,
657
+ customerId,
658
+ installId: normalizeText(entry.installId),
659
+ traceId: normalizeText(entry.traceId),
660
+ amountCents: null,
661
+ currency: null,
662
+ amountKnown: false,
663
+ recurringInterval: null,
664
+ attribution,
665
+ ...extractJourneyFields({
666
+ ...metadata,
667
+ ...sanitizeMetadata(entry),
668
+ ...sanitizeMetadata(entry.attribution),
669
+ }),
670
+ metadata: {
671
+ ...metadata,
672
+ derivedFromPaidProviderEvent: true,
673
+ },
674
+ };
675
+ }
676
+
677
+ function loadResolvedRevenueEvents(options = {}) {
678
+ const analyticsWindow = resolveAnalyticsWindow(options);
679
+ const extraRevenueEvents = Array.isArray(options.extraRevenueEvents) ? options.extraRevenueEvents : [];
680
+ const revenueEvents = filterEntriesForWindow(
681
+ loadRevenueLedger(),
682
+ analyticsWindow,
683
+ (entry) => entry && entry.timestamp
684
+ ).map((entry) => resolveGithubMarketplaceRevenueEntry(entry, { annotate: false }).entry);
685
+ const paidProviderEvents = filterEntriesForWindow(
686
+ loadFunnelLedger(),
687
+ analyticsWindow,
688
+ (entry) => entry && entry.timestamp
689
+ ).filter((entry) => entry && entry.stage === 'paid');
690
+ const resolved = [...revenueEvents];
691
+
692
+ for (const entry of paidProviderEvents) {
693
+ const derived = deriveRevenueEventFromPaidProviderEvent(entry);
694
+ if (!derived) continue;
695
+ if (hasRevenueEventMatch(resolved, derived)) continue;
696
+ resolved.push(derived);
697
+ }
698
+
699
+ return mergeRevenueEvents(resolved, extraRevenueEvents);
700
+ }
701
+
702
+ function repairGithubMarketplaceRevenueLedger(options = {}) {
703
+ const write = Boolean(options.write);
704
+ const ledgerPath = resolveRevenueLedgerFilePath();
705
+ const rows = loadJsonlFile(ledgerPath);
706
+ const resolvedAt = new Date().toISOString();
707
+ const repairs = [];
708
+ const updatedRows = rows.map((entry) => {
709
+ const result = resolveGithubMarketplaceRevenueEntry(entry, {
710
+ annotate: true,
711
+ resolvedAt,
712
+ });
713
+ if (!result.changed) {
714
+ return entry;
715
+ }
716
+ const metadata = sanitizeMetadata(result.entry.metadata);
717
+ repairs.push({
718
+ orderId: normalizeText(result.entry.orderId),
719
+ customerId: normalizeText(result.entry.customerId),
720
+ planId: normalizeText(metadata.planId ?? result.entry.planId),
721
+ amountCents: normalizeInteger(result.entry.amountCents),
722
+ currency: normalizeCurrency(result.entry.currency),
723
+ recurringInterval: normalizeText(result.entry.recurringInterval),
724
+ pricingSource: normalizeText(metadata.githubMarketplaceAmountSource),
725
+ });
726
+ return result.entry;
727
+ });
728
+
729
+ const writeResult = write && repairs.length > 0
730
+ ? writeJsonlRecords(ledgerPath, updatedRows)
731
+ : { written: false, rowCount: rows.length };
732
+
733
+ return {
734
+ ledgerPath,
735
+ write,
736
+ wrote: Boolean(writeResult.written),
737
+ scanned: rows.length,
738
+ repaired: repairs.length,
739
+ unchanged: rows.length - repairs.length,
740
+ repairs,
741
+ writeResult,
742
+ };
743
+ }
744
+
745
+ function appendRevenueEvent({
746
+ provider,
747
+ event,
748
+ status = 'paid',
749
+ customerId,
750
+ orderId = null,
751
+ installId = null,
752
+ traceId = null,
753
+ evidence = null,
754
+ amountCents = null,
755
+ currency = null,
756
+ amountKnown = false,
757
+ recurringInterval = null,
758
+ attribution = {},
759
+ metadata = {},
760
+ } = {}) {
761
+ if (!provider || !event || !customerId) {
762
+ return { written: false, reason: 'missing_required_fields' };
763
+ }
764
+
765
+ const normalizedAmount = normalizeInteger(amountCents);
766
+ const journeyFields = extractJourneyFields({
767
+ ...sanitizeMetadata(metadata),
768
+ ...sanitizeMetadata(attribution),
769
+ });
770
+ const payload = {
771
+ timestamp: new Date().toISOString(),
772
+ provider: normalizeText(provider),
773
+ event,
774
+ status: normalizeText(status) || 'paid',
775
+ orderId: normalizeText(orderId) || normalizeText(evidence) || null,
776
+ evidence: evidence || orderId || event,
777
+ customerId,
778
+ installId: installId || null,
779
+ traceId: traceId || metadata.traceId || null,
780
+ amountCents: normalizedAmount,
781
+ currency: normalizeCurrency(currency),
782
+ amountKnown: Boolean(amountKnown && normalizedAmount !== null),
783
+ recurringInterval: normalizeText(recurringInterval),
784
+ attribution: extractAttribution({ ...sanitizeMetadata(metadata), ...sanitizeMetadata(attribution) }),
785
+ ...journeyFields,
786
+ metadata: sanitizeMetadata(metadata),
787
+ };
788
+
789
+ return appendJsonlRecord(CONFIG.REVENUE_LEDGER_PATH, payload);
790
+ }
791
+
792
+ function loadLocalCheckoutSessions() {
793
+ try {
794
+ const primary = CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH;
795
+ const legacy = resolveLegacyBillingPath('local-checkout-sessions.json');
796
+ const target = fs.existsSync(primary) ? primary : legacy;
797
+ if (!fs.existsSync(target)) return { sessions: {} };
798
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf-8'));
799
+ return (parsed && typeof parsed.sessions === 'object') ? parsed : { sessions: {} };
800
+ } catch { return { sessions: {} }; }
801
+ }
802
+
803
+ function saveLocalCheckoutSessions(store) {
804
+ const target = CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH;
805
+ ensureParentDir(target);
806
+ fs.writeFileSync(target, JSON.stringify(store, null, 2), 'utf-8');
807
+ }
808
+
809
+ function serializeStripeMetadata(metadata) {
810
+ const safe = sanitizeMetadata(metadata);
811
+ const serialized = {};
812
+ for (const [key, value] of Object.entries(safe)) {
813
+ if (value === undefined || value === null) continue;
814
+ if (typeof value === 'object') continue;
815
+ serialized[key] = String(value);
816
+ }
817
+ return serialized;
818
+ }
819
+
820
+ function resolveSubscriptionCheckoutSelection(checkoutMetadata = {}) {
821
+ const planId = normalizePlanId(checkoutMetadata.planId);
822
+ const billingCycle = normalizeBillingCycle(checkoutMetadata.billingCycle);
823
+
824
+ if (planId === 'team') {
825
+ const seatCount = normalizeSeatCount(checkoutMetadata.seatCount, TEAM_MIN_SEATS);
826
+ return {
827
+ planId: 'team',
828
+ billingCycle: 'monthly',
829
+ seatCount,
830
+ quantity: seatCount,
831
+ priceId: CONFIG.STRIPE_PRICE_ID_TEAM_MONTHLY,
832
+ unitPriceDollars: TEAM_MONTHLY_PRICE_DOLLARS,
833
+ totalPriceDollars: TEAM_MONTHLY_PRICE_DOLLARS * seatCount,
834
+ };
835
+ }
836
+
837
+ if (billingCycle === 'annual') {
838
+ return {
839
+ planId: 'pro',
840
+ billingCycle: 'annual',
841
+ seatCount: 1,
842
+ quantity: 1,
843
+ priceId: CONFIG.STRIPE_PRICE_ID_PRO_ANNUAL,
844
+ unitPriceDollars: PRO_ANNUAL_PRICE_DOLLARS,
845
+ totalPriceDollars: PRO_ANNUAL_PRICE_DOLLARS,
846
+ };
847
+ }
848
+
849
+ return {
850
+ planId: 'pro',
851
+ billingCycle: 'monthly',
852
+ seatCount: 1,
853
+ quantity: 1,
854
+ priceId: CONFIG.STRIPE_PRICE_ID_PRO_MONTHLY || CONFIG.STRIPE_PRICE_ID,
855
+ unitPriceDollars: PRO_MONTHLY_PRICE_DOLLARS,
856
+ totalPriceDollars: PRO_MONTHLY_PRICE_DOLLARS,
857
+ };
858
+ }
859
+
860
+ function parseGithubPlanPricing() {
861
+ if (!CONFIG.GITHUB_MARKETPLACE_PLAN_PRICES_JSON) return {};
862
+ try {
863
+ const parsed = JSON.parse(CONFIG.GITHUB_MARKETPLACE_PLAN_PRICES_JSON);
864
+ return parsed && typeof parsed === 'object' ? parsed : {};
865
+ } catch {
866
+ return {};
867
+ }
868
+ }
869
+
870
+ function resolveGithubWebhookPlanPricing(marketplacePurchase) {
871
+ if (!marketplacePurchase || typeof marketplacePurchase !== 'object') {
872
+ return { amountKnown: false, amountCents: null, currency: null, recurringInterval: null, pricingSource: 'unknown' };
873
+ }
874
+
875
+ const plan = marketplacePurchase.plan && typeof marketplacePurchase.plan === 'object'
876
+ ? marketplacePurchase.plan
877
+ : {};
878
+ const billingCycle = normalizeText(marketplacePurchase.billing_cycle ?? marketplacePurchase.billingCycle);
879
+ const monthlyPriceCents = normalizeInteger(plan.monthly_price_in_cents ?? plan.monthlyPriceInCents);
880
+ const yearlyPriceCents = normalizeInteger(plan.yearly_price_in_cents ?? plan.yearlyPriceInCents);
881
+ const priceModel = normalizeText(plan.price_model ?? plan.priceModel);
882
+ const unitCount = normalizeInteger(marketplacePurchase.unit_count ?? marketplacePurchase.unitCount);
883
+
884
+ let amountCents = null;
885
+ let recurringInterval = null;
886
+ const normalizedCycle = billingCycle ? billingCycle.toLowerCase() : null;
887
+
888
+ if (normalizedCycle === 'monthly' || normalizedCycle === 'month') {
889
+ amountCents = monthlyPriceCents;
890
+ recurringInterval = 'month';
891
+ } else if (normalizedCycle === 'yearly' || normalizedCycle === 'annual' || normalizedCycle === 'year') {
892
+ amountCents = yearlyPriceCents;
893
+ recurringInterval = 'year';
894
+ } else if (monthlyPriceCents !== null && yearlyPriceCents === null) {
895
+ amountCents = monthlyPriceCents;
896
+ recurringInterval = 'month';
897
+ } else if (yearlyPriceCents !== null && monthlyPriceCents === null) {
898
+ amountCents = yearlyPriceCents;
899
+ recurringInterval = 'year';
900
+ }
901
+
902
+ if (amountCents !== null && priceModel && priceModel.toUpperCase() === 'PER_UNIT') {
903
+ if (unitCount === null) {
904
+ return { amountKnown: false, amountCents: null, currency: null, recurringInterval, pricingSource: 'unknown' };
905
+ }
906
+ amountCents *= unitCount;
907
+ }
908
+
909
+ return {
910
+ amountKnown: amountCents !== null,
911
+ amountCents,
912
+ currency: amountCents !== null ? 'USD' : null,
913
+ recurringInterval,
914
+ pricingSource: amountCents !== null ? 'webhook' : 'unknown',
915
+ };
916
+ }
917
+
918
+ function resolveGithubPlanPricing(planId, marketplacePurchase = null) {
919
+ const webhookPricing = resolveGithubWebhookPlanPricing(marketplacePurchase);
920
+ if (webhookPricing.amountKnown) {
921
+ return webhookPricing;
922
+ }
923
+
924
+ const pricing = parseGithubPlanPricing();
925
+ const raw = pricing[String(planId)];
926
+ if (raw === undefined) {
927
+ return { amountKnown: false, amountCents: null, currency: null, recurringInterval: null, pricingSource: 'unknown' };
928
+ }
929
+
930
+ if (typeof raw === 'number') {
931
+ return {
932
+ amountKnown: Number.isFinite(raw),
933
+ amountCents: normalizeInteger(raw),
934
+ currency: 'USD',
935
+ recurringInterval: null,
936
+ pricingSource: 'configured_plan_price',
937
+ };
938
+ }
939
+
940
+ if (!raw || typeof raw !== 'object') {
941
+ return { amountKnown: false, amountCents: null, currency: null, recurringInterval: null, pricingSource: 'unknown' };
942
+ }
943
+
944
+ const amountCents = normalizeInteger(raw.amountCents ?? raw.amount ?? raw.priceCents);
945
+ return {
946
+ amountKnown: amountCents !== null,
947
+ amountCents,
948
+ currency: normalizeCurrency(raw.currency) || 'USD',
949
+ recurringInterval: normalizeText(raw.recurringInterval || raw.interval),
950
+ pricingSource: amountCents !== null ? 'configured_plan_price' : 'unknown',
951
+ };
952
+ }
953
+
954
+ function buildGithubMarketplacePurchaseFromMetadata(entry = {}) {
955
+ const metadata = sanitizeMetadata(entry.metadata);
956
+ const billingCycle = normalizeText(
957
+ metadata.billingCycle ??
958
+ metadata.billing_cycle ??
959
+ entry.billingCycle ??
960
+ entry.billing_cycle
961
+ );
962
+ const unitCount = normalizeInteger(metadata.unitCount ?? metadata.unit_count ?? entry.unitCount ?? entry.unit_count);
963
+ const monthlyPriceInCents = normalizeInteger(
964
+ metadata.monthlyPriceInCents ??
965
+ metadata.monthly_price_in_cents ??
966
+ entry.monthlyPriceInCents ??
967
+ entry.monthly_price_in_cents
968
+ );
969
+ const yearlyPriceInCents = normalizeInteger(
970
+ metadata.yearlyPriceInCents ??
971
+ metadata.yearly_price_in_cents ??
972
+ entry.yearlyPriceInCents ??
973
+ entry.yearly_price_in_cents
974
+ );
975
+ const priceModel = normalizeText(
976
+ metadata.priceModel ??
977
+ metadata.price_model ??
978
+ entry.priceModel ??
979
+ entry.price_model
980
+ );
981
+ const planId = normalizeText(metadata.planId ?? entry.planId);
982
+ const planName = normalizeText(metadata.planName ?? entry.planName);
983
+
984
+ if (!billingCycle && unitCount === null && monthlyPriceInCents === null && yearlyPriceInCents === null && !priceModel && !planId && !planName) {
985
+ return null;
986
+ }
987
+
988
+ return {
989
+ billing_cycle: billingCycle,
990
+ unit_count: unitCount,
991
+ plan: {
992
+ id: planId,
993
+ name: planName,
994
+ monthly_price_in_cents: monthlyPriceInCents,
995
+ yearly_price_in_cents: yearlyPriceInCents,
996
+ price_model: priceModel,
997
+ },
998
+ };
999
+ }
1000
+
1001
+ function resolveGithubMarketplaceRevenueEntry(entry = {}, options = {}) {
1002
+ if (!entry || normalizeText(entry.provider) !== 'github_marketplace') {
1003
+ return { changed: false, entry };
1004
+ }
1005
+
1006
+ if (normalizeText(entry.status) !== 'paid') {
1007
+ return { changed: false, entry };
1008
+ }
1009
+
1010
+ if (Boolean(entry.amountKnown) && normalizeInteger(entry.amountCents) !== null) {
1011
+ return { changed: false, entry };
1012
+ }
1013
+
1014
+ const metadata = sanitizeMetadata(entry.metadata);
1015
+ const marketplacePurchase = buildGithubMarketplacePurchaseFromMetadata(entry);
1016
+ const planPricing = resolveGithubPlanPricing(metadata.planId ?? entry.planId, marketplacePurchase);
1017
+ if (!planPricing.amountKnown) {
1018
+ return { changed: false, entry };
1019
+ }
1020
+
1021
+ const resolvedAt = options.resolvedAt || new Date().toISOString();
1022
+ const updatedMetadata = {
1023
+ ...metadata,
1024
+ githubMarketplaceAmountSource: planPricing.pricingSource,
1025
+ };
1026
+ if (options.annotate !== false) {
1027
+ updatedMetadata.githubMarketplaceAmountResolvedAt = resolvedAt;
1028
+ }
1029
+
1030
+ return {
1031
+ changed: true,
1032
+ entry: {
1033
+ ...entry,
1034
+ amountCents: planPricing.amountCents,
1035
+ currency: planPricing.currency || normalizeCurrency(entry.currency),
1036
+ amountKnown: true,
1037
+ recurringInterval: planPricing.recurringInterval || normalizeText(entry.recurringInterval),
1038
+ metadata: updatedMetadata,
1039
+ },
1040
+ pricingSource: planPricing.pricingSource,
1041
+ };
1042
+ }
1043
+
1044
+ function parseTestStripeReconciledRevenueEvents() {
1045
+ const raw = process.env._TEST_STRIPE_RECONCILED_REVENUE_EVENTS_JSON;
1046
+ if (!raw) return [];
1047
+
1048
+ try {
1049
+ const parsed = JSON.parse(raw);
1050
+ return Array.isArray(parsed) ? parsed.filter((entry) => entry && typeof entry === 'object') : [];
1051
+ } catch {
1052
+ return [];
1053
+ }
1054
+ }
1055
+
1056
+ function mergeRevenueEvents(entries = [], extraEntries = []) {
1057
+ const merged = [...entries];
1058
+
1059
+ for (const entry of extraEntries) {
1060
+ if (!entry || typeof entry !== 'object') continue;
1061
+ if (hasRevenueEventMatch(merged, entry)) continue;
1062
+ merged.push(entry);
1063
+ }
1064
+
1065
+ return merged;
1066
+ }
1067
+
1068
+ function buildStripePriceCatalog(currentPrice, relatedPrices = []) {
1069
+ const productId = normalizeText(CONFIG.STRIPE_PRODUCT_ID || (currentPrice && currentPrice.product));
1070
+ const prices = new Map();
1071
+
1072
+ function addPrice(price) {
1073
+ if (!price || typeof price !== 'object') return;
1074
+ const priceId = normalizeText(price.id);
1075
+ if (!priceId) return;
1076
+ prices.set(priceId, {
1077
+ priceId,
1078
+ productId: normalizeText(price.product) || productId,
1079
+ unitAmount: normalizeInteger(price.unit_amount),
1080
+ recurringInterval: normalizeText(price.recurring && price.recurring.interval),
1081
+ active: Boolean(price.active),
1082
+ });
1083
+ }
1084
+
1085
+ addPrice(currentPrice);
1086
+ for (const price of relatedPrices) {
1087
+ addPrice(price);
1088
+ }
1089
+
1090
+ return {
1091
+ productId,
1092
+ prices,
1093
+ };
1094
+ }
1095
+
1096
+ function matchStripeInvoiceLine(priceCatalog, line = {}) {
1097
+ const price = line.price || {};
1098
+ const priceId = normalizeText(price.id);
1099
+ const productId = normalizeText(price.product);
1100
+
1101
+ if (priceId && priceCatalog.prices.has(priceId)) {
1102
+ return priceCatalog.prices.get(priceId);
1103
+ }
1104
+
1105
+ if (productId && priceCatalog.productId && productId === priceCatalog.productId) {
1106
+ return {
1107
+ priceId: priceId || null,
1108
+ productId,
1109
+ unitAmount: normalizeInteger(price.unit_amount),
1110
+ recurringInterval: normalizeText(price.recurring && price.recurring.interval),
1111
+ active: Boolean(price.active),
1112
+ };
1113
+ }
1114
+
1115
+ return null;
1116
+ }
1117
+
1118
+ function matchStripeChargeFromSubscriptions(priceCatalog, charge, subscriptions = []) {
1119
+ if (!charge || !Array.isArray(subscriptions) || subscriptions.length === 0) {
1120
+ return null;
1121
+ }
1122
+
1123
+ const matches = [];
1124
+ for (const subscription of subscriptions) {
1125
+ const items = subscription && subscription.items && Array.isArray(subscription.items.data)
1126
+ ? subscription.items.data
1127
+ : [];
1128
+ for (const item of items) {
1129
+ const price = item.price || {};
1130
+ const priceId = normalizeText(price.id);
1131
+ const productId = normalizeText(price.product);
1132
+ const unitAmount = normalizeInteger(price.unit_amount);
1133
+ const recurringInterval = normalizeText(price.recurring && price.recurring.interval);
1134
+ const matchesConfiguredPrice = priceId && priceCatalog.prices.has(priceId);
1135
+ const matchesConfiguredProduct = productId && priceCatalog.productId && productId === priceCatalog.productId;
1136
+
1137
+ if (!matchesConfiguredPrice && !matchesConfiguredProduct) {
1138
+ continue;
1139
+ }
1140
+
1141
+ matches.push({
1142
+ priceId: priceId || null,
1143
+ productId: productId || priceCatalog.productId,
1144
+ unitAmount,
1145
+ recurringInterval,
1146
+ subscriptionId: normalizeText(subscription.id),
1147
+ });
1148
+ }
1149
+ }
1150
+
1151
+ if (matches.length === 0) {
1152
+ return null;
1153
+ }
1154
+
1155
+ const exactAmountMatch = matches.find((candidate) => {
1156
+ return candidate.unitAmount !== null && candidate.unitAmount === normalizeInteger(charge.amount);
1157
+ });
1158
+ if (exactAmountMatch) {
1159
+ return exactAmountMatch;
1160
+ }
1161
+
1162
+ const description = normalizeText(charge.description || '') || '';
1163
+ return description.toLowerCase().startsWith('subscription') ? matches[0] : null;
1164
+ }
1165
+
1166
+ function buildStripeReconciledRevenueEvent(charge, match = {}) {
1167
+ const timestampMs = Number(charge.created) * 1000;
1168
+ const timestamp = Number.isFinite(timestampMs)
1169
+ ? new Date(timestampMs).toISOString()
1170
+ : new Date().toISOString();
1171
+ const amountCents = normalizeInteger(charge.amount);
1172
+
1173
+ return {
1174
+ timestamp,
1175
+ provider: 'stripe',
1176
+ event: 'stripe_charge_reconciled',
1177
+ status: 'paid',
1178
+ orderId: normalizeText(charge.id),
1179
+ evidence: normalizeText(charge.id) || 'stripe_charge_reconciled',
1180
+ customerId: normalizeText(charge.customer) || `stripe_charge_${normalizeText(charge.id) || 'unknown'}`,
1181
+ installId: null,
1182
+ traceId: null,
1183
+ amountCents,
1184
+ currency: normalizeCurrency(charge.currency),
1185
+ amountKnown: amountCents !== null,
1186
+ recurringInterval: normalizeText(match.recurringInterval),
1187
+ attribution: {
1188
+ source: 'stripe_reconciled',
1189
+ },
1190
+ metadata: {
1191
+ stripeReconciled: true,
1192
+ chargeId: normalizeText(charge.id),
1193
+ paymentIntentId: normalizeText(charge.payment_intent),
1194
+ invoiceId: normalizeText(charge.invoice),
1195
+ priceId: normalizeText(match.priceId),
1196
+ productId: normalizeText(match.productId),
1197
+ subscriptionId: normalizeText(match.subscriptionId),
1198
+ historicalPrice: Boolean(match.priceId && match.priceId !== CONFIG.STRIPE_PRICE_ID),
1199
+ },
1200
+ };
1201
+ }
1202
+
1203
+ async function listStripeReconciledRevenueEvents() {
1204
+ const testEvents = parseTestStripeReconciledRevenueEvents();
1205
+ if (testEvents.length > 0) {
1206
+ return testEvents;
1207
+ }
1208
+
1209
+ if (!CONFIG.STRIPE_SECRET_KEY || !CONFIG.STRIPE_PRICE_ID) {
1210
+ return [];
1211
+ }
1212
+
1213
+ let stripe;
1214
+ try {
1215
+ stripe = getStripeClient();
1216
+ } catch {
1217
+ return [];
1218
+ }
1219
+
1220
+ const currentPrice = await withTimeout(stripe.prices.retrieve(CONFIG.STRIPE_PRICE_ID));
1221
+ const relatedPrices = await withTimeout(stripe.prices.list({
1222
+ product: CONFIG.STRIPE_PRODUCT_ID || currentPrice.product,
1223
+ limit: 100,
1224
+ }));
1225
+ const priceCatalog = buildStripePriceCatalog(
1226
+ currentPrice,
1227
+ relatedPrices && Array.isArray(relatedPrices.data) ? relatedPrices.data : []
1228
+ );
1229
+ if (!priceCatalog.productId) {
1230
+ return [];
1231
+ }
1232
+
1233
+ const charges = await withTimeout(stripe.charges.list({ limit: 100 }));
1234
+ const reconciled = [];
1235
+ const invoiceCache = new Map();
1236
+ const subscriptionCache = new Map();
1237
+
1238
+ for (const charge of charges.data || []) {
1239
+ if (!charge || !charge.paid || charge.status !== 'succeeded' || charge.refunded) {
1240
+ continue;
1241
+ }
1242
+
1243
+ let match = null;
1244
+
1245
+ if (charge.invoice) {
1246
+ const invoiceId = normalizeText(charge.invoice);
1247
+ if (invoiceId) {
1248
+ let invoice = invoiceCache.get(invoiceId);
1249
+ if (!invoice) {
1250
+ invoice = await withTimeout(stripe.invoices.retrieve(invoiceId, { expand: ['lines.data.price'] }));
1251
+ invoiceCache.set(invoiceId, invoice);
1252
+ }
1253
+ const lines = invoice && invoice.lines && Array.isArray(invoice.lines.data) ? invoice.lines.data : [];
1254
+ match = lines.map((line) => matchStripeInvoiceLine(priceCatalog, line)).find(Boolean) || null;
1255
+ }
1256
+ }
1257
+
1258
+ if (!match && charge.customer) {
1259
+ const customerId = normalizeText(charge.customer);
1260
+ if (customerId) {
1261
+ let subscriptions = subscriptionCache.get(customerId);
1262
+ if (!subscriptions) {
1263
+ const listed = await withTimeout(stripe.subscriptions.list({
1264
+ customer: customerId,
1265
+ status: 'all',
1266
+ limit: 100,
1267
+ }));
1268
+ subscriptions = listed && Array.isArray(listed.data) ? listed.data : [];
1269
+ subscriptionCache.set(customerId, subscriptions);
1270
+ }
1271
+ match = matchStripeChargeFromSubscriptions(priceCatalog, charge, subscriptions);
1272
+ }
1273
+ }
1274
+
1275
+ if (!match) {
1276
+ continue;
1277
+ }
1278
+
1279
+ reconciled.push(buildStripeReconciledRevenueEvent(charge, match));
1280
+ }
1281
+
1282
+ return reconciled;
1283
+ }
1284
+
1285
+ function getFunnelAnalytics(options = {}) {
1286
+ const analyticsWindow = resolveAnalyticsWindow(options);
1287
+ const extraRevenueEvents = Array.isArray(options.extraRevenueEvents) ? options.extraRevenueEvents : [];
1288
+ const events = filterEntriesForWindow(
1289
+ loadFunnelLedger(),
1290
+ analyticsWindow,
1291
+ (entry) => entry && entry.timestamp
1292
+ );
1293
+ const paidOrders = loadResolvedRevenueEvents({ ...analyticsWindow, extraRevenueEvents }).filter((entry) => entry && entry.status === 'paid');
1294
+ const stageCounts = { acquisition: 0, activation: 0, paid: 0 };
1295
+ const eventCounts = {};
1296
+ for (const entry of events) {
1297
+ if (entry && stageCounts.hasOwnProperty(entry.stage)) {
1298
+ stageCounts[entry.stage]++;
1299
+ const key = `${entry.stage}:${entry.event || 'unknown'}`;
1300
+ eventCounts[key] = (eventCounts[key] || 0) + 1;
1301
+ }
1302
+ }
1303
+ return {
1304
+ window: serializeAnalyticsWindow(analyticsWindow),
1305
+ totalEvents: events.length,
1306
+ stageCounts,
1307
+ eventCounts,
1308
+ conversionRates: {
1309
+ acquisitionToActivation: safeRate(stageCounts.activation, stageCounts.acquisition),
1310
+ activationToPaid: safeRate(paidOrders.length, stageCounts.activation),
1311
+ acquisitionToPaid: safeRate(paidOrders.length, stageCounts.acquisition),
1312
+ },
1313
+ paidProviderEvents: stageCounts.paid,
1314
+ };
1315
+ }
1316
+
1317
+ function getBusinessAnalytics(options = {}) {
1318
+ const analyticsWindow = resolveAnalyticsWindow(options);
1319
+ const extraRevenueEvents = Array.isArray(options.extraRevenueEvents) ? options.extraRevenueEvents : [];
1320
+ const { FEEDBACK_DIR } = getFeedbackPaths();
1321
+ const telemetry = getTelemetryAnalytics(FEEDBACK_DIR, analyticsWindow);
1322
+ const sourceDiagnostics = buildBillingSourceDiagnostics(FEEDBACK_DIR);
1323
+ const events = filterEntriesForWindow(
1324
+ loadFunnelLedger(),
1325
+ analyticsWindow,
1326
+ (entry) => entry && entry.timestamp
1327
+ );
1328
+ const revenueEvents = loadResolvedRevenueEvents({ ...analyticsWindow, extraRevenueEvents });
1329
+ const workflowSprintLeads = filterEntriesForWindow(
1330
+ loadWorkflowSprintLeads(),
1331
+ analyticsWindow,
1332
+ (entry) => entry && entry.submittedAt
1333
+ );
1334
+ const funnel = getFunnelAnalytics({ ...analyticsWindow, extraRevenueEvents });
1335
+ const acquisitionEvents = events.filter((entry) => entry && entry.stage === 'acquisition');
1336
+ const paidEvents = events.filter((entry) => entry && entry.stage === 'paid');
1337
+ const paidOrders = revenueEvents.filter((entry) => entry && entry.status === 'paid');
1338
+ const firstPaid = paidEvents[0] || null;
1339
+ const lastPaid = paidEvents[paidEvents.length - 1] || null;
1340
+
1341
+ const signupsBySource = {};
1342
+ const signupsByCampaign = {};
1343
+ const signupsByCreator = {};
1344
+ const signupsByCommunity = {};
1345
+ const signupsByPostId = {};
1346
+ const signupsByCommentId = {};
1347
+ const signupsByCampaignVariant = {};
1348
+ const signupsByOfferCode = {};
1349
+ const acquisitionLeadKeys = new Set();
1350
+ const operatorGeneratedAcquisitionBySource = {};
1351
+ const operatorGeneratedAcquisitionLeadKeys = new Set();
1352
+ for (const entry of acquisitionEvents) {
1353
+ const attribution = extractAttribution({
1354
+ ...sanitizeMetadata(entry.metadata),
1355
+ ...sanitizeMetadata(entry),
1356
+ });
1357
+ const sourceKey = resolveAttributionSource(attribution);
1358
+ const campaignKey = resolveAttributionCampaign(attribution);
1359
+ incrementCounter(signupsBySource, sourceKey);
1360
+ incrementCounter(signupsByCampaign, campaignKey);
1361
+ incrementCounter(signupsByCreator, attribution.creator);
1362
+ incrementCounter(signupsByCommunity, attribution.community);
1363
+ incrementCounter(signupsByPostId, attribution.postId);
1364
+ incrementCounter(signupsByCommentId, attribution.commentId);
1365
+ incrementCounter(signupsByCampaignVariant, attribution.campaignVariant);
1366
+ incrementCounter(signupsByOfferCode, attribution.offerCode);
1367
+ acquisitionLeadKeys.add(resolveAcquisitionLeadKey(entry) || `${entry.timestamp}:${entry.event}`);
1368
+ if (isOperatorGeneratedAcquisitionEntry(entry)) {
1369
+ incrementCounter(operatorGeneratedAcquisitionBySource, sourceKey);
1370
+ operatorGeneratedAcquisitionLeadKeys.add(resolveAcquisitionLeadKey(entry) || `${entry.timestamp}:${entry.event}`);
1371
+ }
1372
+ }
1373
+
1374
+ const paidBySource = {};
1375
+ const paidByCampaign = {};
1376
+ const paidByCreator = {};
1377
+ const paidByCommunity = {};
1378
+ const paidByPostId = {};
1379
+ const paidByCommentId = {};
1380
+ const paidByCampaignVariant = {};
1381
+ const paidByOfferCode = {};
1382
+ const bookedRevenueBySourceCents = {};
1383
+ const bookedRevenueByCampaignCents = {};
1384
+ const bookedRevenueByCreatorCents = {};
1385
+ const bookedRevenueByCommunityCents = {};
1386
+ const bookedRevenueByPostIdCents = {};
1387
+ const bookedRevenueByCommentIdCents = {};
1388
+ const bookedRevenueByCampaignVariantCents = {};
1389
+ const bookedRevenueByOfferCodeCents = {};
1390
+ const bookedRevenueByCtaId = {};
1391
+ const bookedRevenueByLandingPath = {};
1392
+ const bookedRevenueByReferrerHost = {};
1393
+ const bookedRevenueByCurrency = {};
1394
+ const paidCustomerIds = new Set();
1395
+ const revenueByProvider = {};
1396
+ let bookedRevenueCents = 0;
1397
+ let amountKnownOrders = 0;
1398
+ let amountUnknownOrders = 0;
1399
+ let derivedPaidOrders = 0;
1400
+ let paidOrdersToday = 0;
1401
+ let bookedRevenueTodayCents = 0;
1402
+ let processorReconciledOrders = 0;
1403
+ let processorReconciledRevenueCents = 0;
1404
+ let latestPaidAt = null;
1405
+ let latestPaidOrder = null;
1406
+
1407
+ for (const entry of paidOrders) {
1408
+ const providerKey = normalizeText(entry.provider) || 'unknown';
1409
+ const attribution = extractAttribution({
1410
+ ...sanitizeMetadata(entry.attribution || {}),
1411
+ ...sanitizeMetadata(entry),
1412
+ });
1413
+ const sourceKey = resolveAttributionSource(attribution, providerKey);
1414
+ const campaignKey = resolveAttributionCampaign(attribution);
1415
+ incrementCounter(paidBySource, sourceKey);
1416
+ incrementCounter(paidByCampaign, campaignKey);
1417
+ incrementCounter(paidByCreator, attribution.creator);
1418
+ incrementCounter(paidByCommunity, attribution.community);
1419
+ incrementCounter(paidByPostId, attribution.postId);
1420
+ incrementCounter(paidByCommentId, attribution.commentId);
1421
+ incrementCounter(paidByCampaignVariant, attribution.campaignVariant);
1422
+ incrementCounter(paidByOfferCode, attribution.offerCode);
1423
+ paidCustomerIds.add(entry.customerId);
1424
+
1425
+ if (!revenueByProvider[providerKey]) {
1426
+ revenueByProvider[providerKey] = {
1427
+ paidOrders: 0,
1428
+ bookedRevenueCents: 0,
1429
+ amountKnownOrders: 0,
1430
+ amountUnknownOrders: 0,
1431
+ bookedRevenueByCurrency: {},
1432
+ };
1433
+ }
1434
+
1435
+ const providerSummary = revenueByProvider[providerKey];
1436
+ providerSummary.paidOrders += 1;
1437
+
1438
+ if (entry.amountKnown && Number.isInteger(entry.amountCents)) {
1439
+ const currency = normalizeCurrency(entry.currency) || 'UNKNOWN';
1440
+ amountKnownOrders += 1;
1441
+ bookedRevenueCents += entry.amountCents;
1442
+ if (eventOccursInWindow(entry.timestamp, {
1443
+ window: 'today',
1444
+ timeZone: analyticsWindow.timeZone,
1445
+ now: analyticsWindow.now,
1446
+ })) {
1447
+ paidOrdersToday += 1;
1448
+ bookedRevenueTodayCents += entry.amountCents;
1449
+ }
1450
+ incrementCounter(bookedRevenueBySourceCents, sourceKey, entry.amountCents);
1451
+ incrementCounter(bookedRevenueByCampaignCents, campaignKey, entry.amountCents);
1452
+ incrementCounter(bookedRevenueByCreatorCents, attribution.creator, entry.amountCents);
1453
+ incrementCounter(bookedRevenueByCommunityCents, attribution.community, entry.amountCents);
1454
+ incrementCounter(bookedRevenueByPostIdCents, attribution.postId, entry.amountCents);
1455
+ incrementCounter(bookedRevenueByCommentIdCents, attribution.commentId, entry.amountCents);
1456
+ incrementCounter(bookedRevenueByCampaignVariantCents, attribution.campaignVariant, entry.amountCents);
1457
+ incrementCounter(bookedRevenueByOfferCodeCents, attribution.offerCode, entry.amountCents);
1458
+ incrementCounter(bookedRevenueByCtaId, entry.ctaId, entry.amountCents);
1459
+ incrementCounter(bookedRevenueByLandingPath, entry.landingPath, entry.amountCents);
1460
+ incrementCounter(bookedRevenueByReferrerHost, entry.referrerHost, entry.amountCents);
1461
+ incrementCounter(bookedRevenueByCurrency, currency, entry.amountCents);
1462
+ providerSummary.bookedRevenueCents += entry.amountCents;
1463
+ providerSummary.amountKnownOrders += 1;
1464
+ incrementCounter(providerSummary.bookedRevenueByCurrency, currency, entry.amountCents);
1465
+ } else {
1466
+ amountUnknownOrders += 1;
1467
+ providerSummary.amountUnknownOrders += 1;
1468
+ }
1469
+
1470
+ if (entry.metadata && entry.metadata.derivedFromPaidProviderEvent) {
1471
+ derivedPaidOrders += 1;
1472
+ }
1473
+ if (entry.metadata && entry.metadata.stripeReconciled) {
1474
+ processorReconciledOrders += 1;
1475
+ if (entry.amountKnown && Number.isInteger(entry.amountCents)) {
1476
+ processorReconciledRevenueCents += entry.amountCents;
1477
+ }
1478
+ }
1479
+
1480
+ if (!latestPaidAt || String(entry.timestamp || '') > latestPaidAt) {
1481
+ latestPaidAt = entry.timestamp || null;
1482
+ latestPaidOrder = {
1483
+ timestamp: entry.timestamp || null,
1484
+ provider: entry.provider || null,
1485
+ event: entry.event || null,
1486
+ orderId: entry.orderId || null,
1487
+ customerId: entry.customerId || null,
1488
+ amountCents: entry.amountCents ?? null,
1489
+ currency: entry.currency || null,
1490
+ amountKnown: Boolean(entry.amountKnown),
1491
+ };
1492
+ }
1493
+ }
1494
+
1495
+ const conversionBySource = {};
1496
+ for (const sourceKey of new Set([...Object.keys(signupsBySource), ...Object.keys(paidBySource)])) {
1497
+ conversionBySource[sourceKey] = safeRate(paidBySource[sourceKey] || 0, signupsBySource[sourceKey] || 0);
1498
+ }
1499
+
1500
+ const conversionByCampaign = {};
1501
+ for (const campaignKey of new Set([...Object.keys(signupsByCampaign), ...Object.keys(paidByCampaign)])) {
1502
+ conversionByCampaign[campaignKey] = safeRate(paidByCampaign[campaignKey] || 0, signupsByCampaign[campaignKey] || 0);
1503
+ }
1504
+
1505
+ const conversionByCreator = {};
1506
+ for (const creatorKey of new Set([...Object.keys(signupsByCreator), ...Object.keys(paidByCreator)])) {
1507
+ conversionByCreator[creatorKey] = safeRate(paidByCreator[creatorKey] || 0, signupsByCreator[creatorKey] || 0);
1508
+ }
1509
+
1510
+ const conversionByCommunity = {};
1511
+ for (const communityKey of new Set([...Object.keys(signupsByCommunity), ...Object.keys(paidByCommunity)])) {
1512
+ conversionByCommunity[communityKey] = safeRate(paidByCommunity[communityKey] || 0, signupsByCommunity[communityKey] || 0);
1513
+ }
1514
+
1515
+ const conversionByPostId = {};
1516
+ for (const postId of new Set([...Object.keys(signupsByPostId), ...Object.keys(paidByPostId)])) {
1517
+ conversionByPostId[postId] = safeRate(paidByPostId[postId] || 0, signupsByPostId[postId] || 0);
1518
+ }
1519
+
1520
+ const conversionByCommentId = {};
1521
+ for (const commentId of new Set([...Object.keys(signupsByCommentId), ...Object.keys(paidByCommentId)])) {
1522
+ conversionByCommentId[commentId] = safeRate(paidByCommentId[commentId] || 0, signupsByCommentId[commentId] || 0);
1523
+ }
1524
+
1525
+ const conversionByCampaignVariant = {};
1526
+ for (const variant of new Set([...Object.keys(signupsByCampaignVariant), ...Object.keys(paidByCampaignVariant)])) {
1527
+ conversionByCampaignVariant[variant] = safeRate(paidByCampaignVariant[variant] || 0, signupsByCampaignVariant[variant] || 0);
1528
+ }
1529
+
1530
+ const conversionByOfferCode = {};
1531
+ for (const offerCode of new Set([...Object.keys(signupsByOfferCode), ...Object.keys(paidByOfferCode)])) {
1532
+ conversionByOfferCode[offerCode] = safeRate(paidByOfferCode[offerCode] || 0, signupsByOfferCode[offerCode] || 0);
1533
+ }
1534
+
1535
+ const workflowSprintLeadStatus = {};
1536
+ const workflowSprintLeadBySource = {};
1537
+ const workflowSprintLeadByCampaign = {};
1538
+ const workflowSprintLeadByCreator = {};
1539
+ const workflowSprintLeadByCommunity = {};
1540
+ const workflowSprintLeadByRuntime = {};
1541
+ const qualifiedWorkflowSprintLeadBySource = {};
1542
+ const qualifiedWorkflowSprintLeadByCreator = {};
1543
+ let workflowSprintLeadLatest = null;
1544
+ let workflowSprintLeadLatestAt = null;
1545
+ let workflowSprintLeadContactable = 0;
1546
+ let qualifiedWorkflowSprintLeadCount = 0;
1547
+
1548
+ for (const entry of workflowSprintLeads) {
1549
+ if (!entry || typeof entry !== 'object') continue;
1550
+ incrementCounter(workflowSprintLeadStatus, entry.status);
1551
+ const attribution = extractAttribution(entry.attribution || {});
1552
+ incrementCounter(workflowSprintLeadBySource, resolveAttributionSource(attribution, 'workflow_sprint_intake'));
1553
+ incrementCounter(workflowSprintLeadByCampaign, resolveAttributionCampaign(attribution));
1554
+ incrementCounter(workflowSprintLeadByCreator, attribution.creator);
1555
+ incrementCounter(workflowSprintLeadByCommunity, attribution.community);
1556
+ incrementCounter(workflowSprintLeadByRuntime, entry.qualification?.runtime);
1557
+
1558
+ if (entry.contact?.email) {
1559
+ workflowSprintLeadContactable += 1;
1560
+ }
1561
+ if (isQualifiedWorkflowSprintLead(entry)) {
1562
+ qualifiedWorkflowSprintLeadCount += 1;
1563
+ incrementCounter(
1564
+ qualifiedWorkflowSprintLeadBySource,
1565
+ resolveAttributionSource(attribution, 'workflow_sprint_intake')
1566
+ );
1567
+ incrementCounter(qualifiedWorkflowSprintLeadByCreator, attribution.creator);
1568
+ }
1569
+
1570
+ if (!workflowSprintLeadLatestAt || String(entry.submittedAt || '') > workflowSprintLeadLatestAt) {
1571
+ workflowSprintLeadLatestAt = entry.submittedAt || null;
1572
+ workflowSprintLeadLatest = {
1573
+ leadId: entry.leadId || null,
1574
+ submittedAt: entry.submittedAt || null,
1575
+ status: entry.status || null,
1576
+ email: entry.contact?.email || null,
1577
+ company: entry.contact?.company || null,
1578
+ workflow: entry.qualification?.workflow || null,
1579
+ owner: entry.qualification?.owner || null,
1580
+ runtime: entry.qualification?.runtime || null,
1581
+ source: attribution.source || null,
1582
+ campaign: attribution.campaign || null,
1583
+ };
1584
+ }
1585
+ }
1586
+
1587
+ const unreconciledPaidEvents = paidEvents.filter((entry) => {
1588
+ const eventKey = resolvePaidProviderEventKey(entry);
1589
+ if (!eventKey) return true;
1590
+ return !paidOrders.some((order) => resolveRevenueEventKey(order) === eventKey);
1591
+ }).length;
1592
+
1593
+ const trafficMetrics = {
1594
+ visitors: telemetry.visitors ? telemetry.visitors.uniqueVisitors || 0 : 0,
1595
+ sessions: telemetry.visitors ? telemetry.visitors.uniqueSessions || 0 : 0,
1596
+ pageViews: telemetry.visitors ? telemetry.visitors.pageViews || 0 : 0,
1597
+ ctaClicks: telemetry.ctas ? telemetry.ctas.totalClicks || 0 : 0,
1598
+ checkoutStarts: telemetry.ctas ? telemetry.ctas.checkoutStarts || 0 : 0,
1599
+ checkoutSuccessPageViews: telemetry.ctas ? telemetry.ctas.successPageViews || 0 : 0,
1600
+ checkoutCancelPageViews: telemetry.ctas ? telemetry.ctas.cancelPageViews || 0 : 0,
1601
+ checkoutPaidConfirmations: telemetry.ctas ? telemetry.ctas.paidConfirmations || 0 : 0,
1602
+ checkoutPendingSessions: telemetry.ctas ? telemetry.ctas.sessionPending || 0 : 0,
1603
+ checkoutLookupFailures: telemetry.ctas ? telemetry.ctas.lookupFailures || 0 : 0,
1604
+ buyerLossFeedback: telemetry.buyerLoss ? telemetry.buyerLoss.totalSignals || 0 : 0,
1605
+ seoLandingViews: telemetry.seo ? telemetry.seo.landingViews || 0 : 0,
1606
+ };
1607
+
1608
+ const operatorGeneratedAcquisition = {
1609
+ totalEvents: acquisitionEvents.filter(isOperatorGeneratedAcquisitionEntry).length,
1610
+ uniqueLeads: operatorGeneratedAcquisitionLeadKeys.size,
1611
+ bySource: operatorGeneratedAcquisitionBySource,
1612
+ };
1613
+
1614
+ const dataQuality = {
1615
+ telemetryCoverage: Number(((
1616
+ (telemetry.visitors ? telemetry.visitors.visitorIdCoverageRate || 0 : 0) +
1617
+ (telemetry.visitors ? telemetry.visitors.sessionIdCoverageRate || 0 : 0) +
1618
+ (telemetry.visitors ? telemetry.visitors.acquisitionIdCoverageRate || 0 : 0)
1619
+ ) / 3).toFixed(4)),
1620
+ attributionCoverage: telemetry.visitors ? telemetry.visitors.attributionCoverageRate || 0 : 0,
1621
+ amountKnownCoverage: paidOrders.length ? safeRate(amountKnownOrders, paidOrders.length) : 0,
1622
+ unreconciledPaidEvents,
1623
+ };
1624
+
1625
+ return {
1626
+ generatedAt: new Date().toISOString(),
1627
+ window: serializeAnalyticsWindow(analyticsWindow),
1628
+ coverage: {
1629
+ source: 'funnel_ledger+revenue_ledger+workflow_sprint_leads',
1630
+ tracksBookedRevenue: true,
1631
+ tracksPaidOrders: true,
1632
+ tracksInvoices: false,
1633
+ tracksAttribution: true,
1634
+ tracksWorkflowSprintLeads: true,
1635
+ providerCoverage: {
1636
+ stripe: processorReconciledOrders > 0 ? 'booked_revenue+processor_reconciled' : 'booked_revenue',
1637
+ githubMarketplace: 'webhook_or_configured_plan_prices',
1638
+ },
1639
+ },
1640
+ funnel: {
1641
+ ...funnel,
1642
+ uniqueAcquisitionLeads: acquisitionLeadKeys.size,
1643
+ uniquePaidCustomers: paidCustomerIds.size,
1644
+ firstPaidAt: firstPaid ? firstPaid.timestamp || null : null,
1645
+ lastPaidAt: lastPaid ? lastPaid.timestamp || null : null,
1646
+ lastPaidEvent: lastPaid ? {
1647
+ timestamp: lastPaid.timestamp || null,
1648
+ event: lastPaid.event || null,
1649
+ evidence: lastPaid.evidence || null,
1650
+ customerId: lastPaid.metadata?.customerId || null,
1651
+ traceId: lastPaid.traceId || null,
1652
+ } : null,
1653
+ },
1654
+ signups: {
1655
+ total: acquisitionEvents.length,
1656
+ uniqueLeads: acquisitionLeadKeys.size,
1657
+ bySource: signupsBySource,
1658
+ byCampaign: signupsByCampaign,
1659
+ byCreator: signupsByCreator,
1660
+ byCommunity: signupsByCommunity,
1661
+ byPostId: signupsByPostId,
1662
+ byCommentId: signupsByCommentId,
1663
+ byCampaignVariant: signupsByCampaignVariant,
1664
+ byOfferCode: signupsByOfferCode,
1665
+ },
1666
+ revenue: {
1667
+ paidProviderEvents: paidEvents.length,
1668
+ paidOrders: paidOrders.length,
1669
+ paidCustomers: paidCustomerIds.size,
1670
+ bookedRevenueCents,
1671
+ bookedRevenueTodayCents,
1672
+ bookedRevenueByCurrency,
1673
+ amountKnownOrders,
1674
+ amountUnknownOrders,
1675
+ derivedPaidOrders,
1676
+ paidOrdersToday,
1677
+ processorReconciledOrders,
1678
+ processorReconciledRevenueCents,
1679
+ amountKnownCoverageRate: safeRate(amountKnownOrders, paidOrders.length),
1680
+ unreconciledPaidEvents,
1681
+ latestPaidAt,
1682
+ latestPaidOrder,
1683
+ byProvider: revenueByProvider,
1684
+ },
1685
+ pipeline: {
1686
+ workflowSprintLeads: {
1687
+ total: workflowSprintLeads.length,
1688
+ contactable: workflowSprintLeadContactable,
1689
+ byStatus: workflowSprintLeadStatus,
1690
+ bySource: workflowSprintLeadBySource,
1691
+ byCampaign: workflowSprintLeadByCampaign,
1692
+ byCreator: workflowSprintLeadByCreator,
1693
+ byCommunity: workflowSprintLeadByCommunity,
1694
+ byRuntime: workflowSprintLeadByRuntime,
1695
+ latestLeadAt: workflowSprintLeadLatestAt,
1696
+ latestLead: workflowSprintLeadLatest,
1697
+ },
1698
+ qualifiedWorkflowSprintLeads: {
1699
+ total: qualifiedWorkflowSprintLeadCount,
1700
+ bySource: qualifiedWorkflowSprintLeadBySource,
1701
+ byCreator: qualifiedWorkflowSprintLeadByCreator,
1702
+ },
1703
+ },
1704
+ attribution: {
1705
+ acquisitionBySource: signupsBySource,
1706
+ acquisitionByCampaign: signupsByCampaign,
1707
+ acquisitionByCreator: signupsByCreator,
1708
+ acquisitionByCommunity: signupsByCommunity,
1709
+ acquisitionByPostId: signupsByPostId,
1710
+ acquisitionByCommentId: signupsByCommentId,
1711
+ acquisitionByCampaignVariant: signupsByCampaignVariant,
1712
+ acquisitionByOfferCode: signupsByOfferCode,
1713
+ paidBySource,
1714
+ paidByCampaign,
1715
+ paidByCreator,
1716
+ paidByCommunity,
1717
+ paidByPostId,
1718
+ paidByCommentId,
1719
+ paidByCampaignVariant,
1720
+ paidByOfferCode,
1721
+ bookedRevenueBySourceCents,
1722
+ bookedRevenueByCampaignCents,
1723
+ bookedRevenueByCreatorCents,
1724
+ bookedRevenueByCommunityCents,
1725
+ bookedRevenueByPostIdCents,
1726
+ bookedRevenueByCommentIdCents,
1727
+ bookedRevenueByCampaignVariantCents,
1728
+ bookedRevenueByOfferCodeCents,
1729
+ bookedRevenueByCtaId,
1730
+ bookedRevenueByLandingPath,
1731
+ bookedRevenueByReferrerHost,
1732
+ conversionBySource,
1733
+ conversionByCampaign,
1734
+ conversionByCreator,
1735
+ conversionByCommunity,
1736
+ conversionByPostId,
1737
+ conversionByCommentId,
1738
+ conversionByCampaignVariant,
1739
+ conversionByOfferCode,
1740
+ },
1741
+ trafficMetrics,
1742
+ operatorGeneratedAcquisition,
1743
+ dataQuality,
1744
+ sourceDiagnostics,
1745
+ };
1746
+ }
1747
+
1748
+ function getBillingSummary(options = {}) {
1749
+ const business = getBusinessAnalytics(options);
1750
+ const store = loadKeyStore();
1751
+ const keyEntries = Object.values(store.keys || {});
1752
+ const customers = new Map();
1753
+ const bySource = {};
1754
+ const activeBySource = {};
1755
+ let activeKeys = 0;
1756
+ let disabledKeys = 0;
1757
+ let totalUsage = 0;
1758
+ const activeCustomerIds = new Set();
1759
+
1760
+ for (const meta of keyEntries) {
1761
+ const source = meta.source || 'unknown';
1762
+ const customerId = meta.customerId || 'unknown';
1763
+ const usageCount = Number(meta.usageCount || 0);
1764
+ bySource[source] = (bySource[source] || 0) + 1;
1765
+ totalUsage += usageCount;
1766
+
1767
+ if (meta.active) {
1768
+ activeKeys += 1;
1769
+ activeBySource[source] = (activeBySource[source] || 0) + 1;
1770
+ activeCustomerIds.add(customerId);
1771
+ } else {
1772
+ disabledKeys += 1;
1773
+ }
1774
+
1775
+ if (!customers.has(customerId)) {
1776
+ customers.set(customerId, {
1777
+ customerId,
1778
+ activeKeys: 0,
1779
+ totalKeys: 0,
1780
+ usageCount: 0,
1781
+ source,
1782
+ installId: meta.installId || null,
1783
+ createdAt: meta.createdAt || null,
1784
+ disabledAt: meta.disabledAt || null,
1785
+ });
1786
+ }
1787
+
1788
+ const summary = customers.get(customerId);
1789
+ summary.totalKeys += 1;
1790
+ summary.usageCount += usageCount;
1791
+ if (meta.active) {
1792
+ summary.activeKeys += 1;
1793
+ }
1794
+ if (meta.source && (!summary.source || summary.source === 'unknown')) {
1795
+ summary.source = meta.source;
1796
+ }
1797
+ if (meta.installId && !summary.installId) {
1798
+ summary.installId = meta.installId;
1799
+ }
1800
+ if (meta.createdAt && (!summary.createdAt || meta.createdAt < summary.createdAt)) {
1801
+ summary.createdAt = meta.createdAt;
1802
+ }
1803
+ if (meta.disabledAt && (!summary.disabledAt || meta.disabledAt > summary.disabledAt)) {
1804
+ summary.disabledAt = meta.disabledAt;
1805
+ }
1806
+ }
1807
+
1808
+ const orderedCustomers = Array.from(customers.values()).sort((a, b) => {
1809
+ const aTime = a.createdAt || '';
1810
+ const bTime = b.createdAt || '';
1811
+ return aTime.localeCompare(bTime) || a.customerId.localeCompare(b.customerId);
1812
+ });
1813
+
1814
+ return {
1815
+ generatedAt: business.generatedAt,
1816
+ window: business.window,
1817
+ coverage: {
1818
+ ...business.coverage,
1819
+ source: 'funnel_ledger+revenue_ledger+key_store+workflow_sprint_leads',
1820
+ },
1821
+ funnel: business.funnel,
1822
+ signups: business.signups,
1823
+ revenue: business.revenue,
1824
+ pipeline: business.pipeline,
1825
+ attribution: business.attribution,
1826
+ trafficMetrics: business.trafficMetrics,
1827
+ operatorGeneratedAcquisition: business.operatorGeneratedAcquisition,
1828
+ dataQuality: business.dataQuality,
1829
+ sourceDiagnostics: business.sourceDiagnostics,
1830
+ keys: {
1831
+ scope: 'current_state',
1832
+ windowed: false,
1833
+ total: keyEntries.length,
1834
+ active: activeKeys,
1835
+ disabled: disabledKeys,
1836
+ activeCustomers: activeCustomerIds.size,
1837
+ totalUsage,
1838
+ bySource,
1839
+ activeBySource,
1840
+ },
1841
+ customers: orderedCustomers,
1842
+ };
1843
+ }
1844
+
1845
+ async function getBillingSummaryLive(options = {}) {
1846
+ try {
1847
+ const extraRevenueEvents = await listStripeReconciledRevenueEvents().catch(() => []);
1848
+ return getBillingSummary({
1849
+ ...options,
1850
+ extraRevenueEvents,
1851
+ });
1852
+ } catch (err) {
1853
+ const isTimeout = err && err.message && err.message.includes('Stripe API timeout');
1854
+ return {
1855
+ error: isTimeout ? 'stripe_timeout' : 'billing_summary_error',
1856
+ message: err && err.message ? err.message : 'Unknown error',
1857
+ revenue: { total: 0, mrr: 0, events: [] },
1858
+ usage: { totalUsage: 0, bySource: {}, activeBySource: {} },
1859
+ customers: [],
1860
+ };
1861
+ }
1862
+ }
1863
+
1864
+ function loadKeyStore() {
1865
+ try {
1866
+ const primary = CONFIG.API_KEYS_PATH;
1867
+ const legacy = resolveLegacyBillingPath('api-keys.json');
1868
+ const target = (IS_TEST || fs.existsSync(primary)) ? primary : legacy;
1869
+ if (!fs.existsSync(target)) return { keys: {} };
1870
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf-8'));
1871
+ return (parsed && typeof parsed.keys === 'object') ? parsed : { keys: {} };
1872
+ } catch { return { keys: {} }; }
1873
+ }
1874
+
1875
+ function saveKeyStore(store) {
1876
+ const target = CONFIG.API_KEYS_PATH;
1877
+ ensureParentDir(target);
1878
+ fs.writeFileSync(target, JSON.stringify(store, null, 2), 'utf-8');
1879
+ }
1880
+
1881
+ // ---------------------------------------------------------------------------
1882
+ // Core Exports
1883
+ // ---------------------------------------------------------------------------
1884
+
1885
+ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, installId, traceId, packId = null, metadata = {} } = {}) {
1886
+ const resolvedTraceId = traceId || metadata.traceId || createTraceId('checkout');
1887
+ const baseCheckoutMetadata = sanitizeMetadata({
1888
+ ...metadata,
1889
+ installId: installId || metadata.installId || 'unknown',
1890
+ traceId: resolvedTraceId,
1891
+ });
1892
+ const checkoutSelection = packId ? null : resolveSubscriptionCheckoutSelection(baseCheckoutMetadata);
1893
+ const checkoutMetadata = packId
1894
+ ? baseCheckoutMetadata
1895
+ : sanitizeMetadata({
1896
+ ...baseCheckoutMetadata,
1897
+ planId: checkoutSelection.planId,
1898
+ billingCycle: checkoutSelection.billingCycle,
1899
+ seatCount: checkoutSelection.seatCount,
1900
+ priceId: checkoutSelection.priceId,
1901
+ });
1902
+ const resolvedInstallId = installId || checkoutMetadata.installId || 'unknown';
1903
+
1904
+ if (LOCAL_MODE()) {
1905
+ const localSessionId = `test_session_${crypto.randomBytes(8).toString('hex')}`;
1906
+ const store = loadLocalCheckoutSessions();
1907
+ const pack = packId ? CONFIG.CREDIT_PACKS[packId] : null;
1908
+ store.sessions[localSessionId] = {
1909
+ id: localSessionId,
1910
+ customer: `local_cus_${crypto.randomBytes(4).toString('hex')}`,
1911
+ metadata: { ...checkoutMetadata, packId: pack ? pack.id : null, credits: pack ? pack.credits : null },
1912
+ payment_status: 'paid',
1913
+ status: 'complete'
1914
+ };
1915
+ saveLocalCheckoutSessions(store);
1916
+
1917
+ appendFunnelEvent({
1918
+ stage: 'acquisition',
1919
+ event: 'checkout_session_created',
1920
+ installId: resolvedInstallId,
1921
+ traceId: resolvedTraceId,
1922
+ evidence: 'local_mode_manual',
1923
+ metadata: { ...checkoutMetadata, packId: pack ? pack.id : null },
1924
+ });
1925
+ return { sessionId: localSessionId, url: null, localMode: true, traceId: resolvedTraceId, metadata: checkoutMetadata };
1926
+ }
1927
+
1928
+ const stripe = getStripeClient();
1929
+ const sessionPayload = buildCheckoutSessionPayload({
1930
+ successUrl,
1931
+ cancelUrl,
1932
+ customerEmail,
1933
+ checkoutMetadata,
1934
+ packId,
1935
+ });
1936
+ const session = await stripe.checkout.sessions.create(sessionPayload);
1937
+
1938
+ appendFunnelEvent({
1939
+ stage: 'acquisition',
1940
+ event: 'checkout_session_created',
1941
+ installId: resolvedInstallId,
1942
+ traceId: resolvedTraceId,
1943
+ evidence: session.id,
1944
+ metadata: { ...checkoutMetadata, packId },
1945
+ });
1946
+ return { sessionId: session.id, url: session.url, localMode: false, traceId: resolvedTraceId, metadata: checkoutMetadata };
1947
+ }
1948
+
1949
+ function buildCheckoutSessionPayload({ successUrl, cancelUrl, customerEmail, checkoutMetadata, packId = null } = {}) {
1950
+ const pack = packId ? CONFIG.CREDIT_PACKS[packId] : null;
1951
+ const checkoutSelection = pack ? null : resolveSubscriptionCheckoutSelection(checkoutMetadata);
1952
+ if (!pack && !checkoutSelection.priceId) {
1953
+ throw new Error(`Stripe price ID is missing for ${checkoutSelection.planId} ${checkoutSelection.billingCycle} checkout.`);
1954
+ }
1955
+ const lineItems = pack
1956
+ ? [{
1957
+ price_data: {
1958
+ currency: pack.currency.toLowerCase(),
1959
+ product_data: { name: pack.name },
1960
+ unit_amount: pack.amountCents,
1961
+ },
1962
+ quantity: 1,
1963
+ }]
1964
+ : [{ price: checkoutSelection.priceId, quantity: checkoutSelection.quantity }];
1965
+
1966
+ const sessionPayload = {
1967
+ success_url: successUrl,
1968
+ cancel_url: cancelUrl,
1969
+ payment_method_types: ['card', 'link'],
1970
+ mode: pack ? 'payment' : 'subscription',
1971
+ line_items: lineItems,
1972
+ metadata: serializeStripeMetadata({
1973
+ ...checkoutMetadata,
1974
+ planId: pack ? checkoutMetadata.planId : checkoutSelection.planId,
1975
+ billingCycle: pack ? checkoutMetadata.billingCycle : checkoutSelection.billingCycle,
1976
+ seatCount: pack ? checkoutMetadata.seatCount : checkoutSelection.seatCount,
1977
+ priceId: pack ? checkoutMetadata.priceId : checkoutSelection.priceId,
1978
+ packId: pack ? pack.id : null,
1979
+ credits: pack ? pack.credits : null,
1980
+ }),
1981
+ // 7-day free trial for subscriptions — reduces checkout abandonment
1982
+ ...(pack ? {} : { subscription_data: { trial_period_days: 7 } }),
1983
+ };
1984
+
1985
+ const normalizedCustomerEmail = normalizeText(customerEmail);
1986
+ if (normalizedCustomerEmail) {
1987
+ sessionPayload.customer_email = normalizedCustomerEmail;
1988
+ }
1989
+ return sessionPayload;
1990
+ }
1991
+
1992
+ async function getCheckoutSessionStatus(sessionId) {
1993
+ if (LOCAL_MODE()) {
1994
+ const store = loadLocalCheckoutSessions();
1995
+ const session = store.sessions[sessionId];
1996
+ if (!session) return { found: false };
1997
+ const provisioned = provisionApiKey(session.customer, {
1998
+ installId: session.metadata?.installId,
1999
+ credits: session.metadata?.credits,
2000
+ source: 'local_checkout_lookup'
2001
+ });
2002
+ return {
2003
+ found: true,
2004
+ localMode: true,
2005
+ sessionId,
2006
+ paid: true,
2007
+ paymentStatus: 'paid',
2008
+ status: 'complete',
2009
+ customerId: session.customer,
2010
+ installId: session.metadata?.installId,
2011
+ traceId: session.metadata?.traceId || null,
2012
+ acquisitionId: session.metadata?.acquisitionId || null,
2013
+ visitorId: session.metadata?.visitorId || null,
2014
+ visitorSessionId: session.metadata?.sessionId || null,
2015
+ ctaId: session.metadata?.ctaId || null,
2016
+ ctaPlacement: session.metadata?.ctaPlacement || null,
2017
+ planId: session.metadata?.planId || session.metadata?.packId || null,
2018
+ landingPath: session.metadata?.landingPath || null,
2019
+ referrerHost: session.metadata?.referrerHost || null,
2020
+ apiKey: provisioned.key,
2021
+ remainingCredits: provisioned.remainingCredits,
2022
+ };
2023
+ }
2024
+
2025
+ try {
2026
+ const stripe = getStripeClient();
2027
+ const session = await stripe.checkout.sessions.retrieve(sessionId);
2028
+ const isPaid = session.payment_status === 'paid' || session.payment_status === 'no_payment_required';
2029
+ const traceId = session.metadata?.traceId || null;
2030
+
2031
+ if (!isPaid) return { found: true, localMode: false, sessionId, paid: false, paymentStatus: session.payment_status, status: session.status };
2032
+
2033
+ const installId = session.metadata?.installId || null;
2034
+ const credits = session.metadata?.credits ? parseInt(session.metadata.credits, 10) : null;
2035
+ const provisioned = provisionApiKey(session.customer, { installId, credits, source: 'stripe_checkout_session_lookup' });
2036
+
2037
+ return {
2038
+ found: true,
2039
+ localMode: false,
2040
+ sessionId,
2041
+ paid: true,
2042
+ paymentStatus: session.payment_status,
2043
+ customerId: session.customer,
2044
+ customerEmail: session.customer_details?.email || '',
2045
+ installId,
2046
+ traceId,
2047
+ acquisitionId: session.metadata?.acquisitionId || null,
2048
+ visitorId: session.metadata?.visitorId || null,
2049
+ visitorSessionId: session.metadata?.sessionId || null,
2050
+ ctaId: session.metadata?.ctaId || null,
2051
+ ctaPlacement: session.metadata?.ctaPlacement || null,
2052
+ planId: session.metadata?.planId || session.metadata?.packId || null,
2053
+ landingPath: session.metadata?.landingPath || null,
2054
+ referrerHost: session.metadata?.referrerHost || null,
2055
+ apiKey: provisioned.key,
2056
+ remainingCredits: provisioned.remainingCredits,
2057
+ };
2058
+ } catch {
2059
+ return { found: false };
2060
+ }
2061
+ }
2062
+
2063
+ function provisionApiKey(customerId, opts = {}) {
2064
+ if (!customerId || typeof customerId !== 'string') throw new Error('customerId is required');
2065
+ const store = loadKeyStore();
2066
+ const existing = Object.entries(store.keys).find(([, m]) => m.customerId === customerId && m.active);
2067
+
2068
+ const creditsToAdd = normalizeInteger(opts.credits);
2069
+
2070
+ if (existing) {
2071
+ const key = existing[0];
2072
+ const meta = existing[1];
2073
+ if (opts.installId && !meta.installId) { meta.installId = opts.installId; }
2074
+ if (creditsToAdd !== null) {
2075
+ meta.remainingCredits = (meta.remainingCredits || 0) + creditsToAdd;
2076
+ }
2077
+ saveKeyStore(store);
2078
+ return { key, customerId, createdAt: meta.createdAt, installId: meta.installId || null, reused: true, remainingCredits: meta.remainingCredits };
2079
+ }
2080
+
2081
+ const key = `tg_${crypto.randomBytes(16).toString('hex')}`;
2082
+ const createdAt = new Date().toISOString();
2083
+ store.keys[key] = {
2084
+ customerId,
2085
+ active: true,
2086
+ usageCount: 0,
2087
+ createdAt,
2088
+ installId: opts.installId || null,
2089
+ source: opts.source || 'provision',
2090
+ remainingCredits: creditsToAdd // null means unlimited (standard subscription)
2091
+ };
2092
+ saveKeyStore(store);
2093
+ return { key, customerId, createdAt, installId: opts.installId || null, remainingCredits: creditsToAdd };
2094
+ }
2095
+
2096
+ function rotateApiKey(oldKey) {
2097
+ if (!oldKey) return { rotated: false, reason: 'missing_old_key' };
2098
+ const store = loadKeyStore();
2099
+ const meta = store.keys[oldKey];
2100
+ if (!meta || !meta.active) return { rotated: false, reason: 'key_not_active' };
2101
+
2102
+ meta.active = false;
2103
+ meta.disabledAt = new Date().toISOString();
2104
+ const newKey = `tg_${crypto.randomBytes(16).toString('hex')}`;
2105
+ store.keys[newKey] = {
2106
+ customerId: meta.customerId,
2107
+ active: true,
2108
+ usageCount: 0,
2109
+ createdAt: new Date().toISOString(),
2110
+ installId: meta.installId,
2111
+ source: 'rotation',
2112
+ replacedKey: oldKey,
2113
+ remainingCredits: meta.remainingCredits
2114
+ };
2115
+ saveKeyStore(store);
2116
+ return { rotated: true, key: newKey, oldKey };
2117
+ }
2118
+
2119
+ function validateApiKey(key) {
2120
+ if (!key) return { valid: false };
2121
+ const store = loadKeyStore();
2122
+ const meta = store.keys[key];
2123
+ if (!meta || !meta.active) return { valid: false };
2124
+
2125
+ // Check if credits are exhausted
2126
+ if (meta.remainingCredits !== undefined && meta.remainingCredits !== null && meta.remainingCredits <= 0) {
2127
+ return { valid: false, reason: 'credits_exhausted' };
2128
+ }
2129
+
2130
+ return {
2131
+ valid: true,
2132
+ customerId: meta.customerId,
2133
+ usageCount: meta.usageCount || 0,
2134
+ installId: meta.installId || null,
2135
+ createdAt: meta.createdAt,
2136
+ metadata: meta,
2137
+ };
2138
+ }
2139
+
2140
+ function recordUsage(key) {
2141
+ const store = loadKeyStore();
2142
+ const meta = store.keys[key];
2143
+ if (meta && meta.active) {
2144
+ const oldVal = meta.usageCount || 0;
2145
+ meta.usageCount = oldVal + 1;
2146
+
2147
+ // Decrement credits if applicable
2148
+ if (meta.remainingCredits !== undefined && meta.remainingCredits !== null) {
2149
+ meta.remainingCredits = Math.max(0, meta.remainingCredits - 1);
2150
+ }
2151
+
2152
+ if (oldVal === 0) appendFunnelEvent({ stage: 'activation', event: 'api_key_first_usage', installId: meta.installId, evidence: key, metadata: { customerId: meta.customerId } });
2153
+ saveKeyStore(store);
2154
+ return { recorded: true, usageCount: meta.usageCount, remainingCredits: meta.remainingCredits };
2155
+ }
2156
+ return { recorded: false };
2157
+ }
2158
+
2159
+ function disableCustomerKeys(customerId) {
2160
+ const store = loadKeyStore();
2161
+ let disabledCount = 0;
2162
+ for (const [key, meta] of Object.entries(store.keys)) {
2163
+ if (meta.customerId === customerId && meta.active) { meta.active = false; meta.disabledAt = new Date().toISOString(); disabledCount++; }
2164
+ }
2165
+ if (disabledCount > 0) saveKeyStore(store);
2166
+ return { disabledCount };
2167
+ }
2168
+
2169
+ function verifyWebhookSignature(rawBody, signature) {
2170
+ if (!CONFIG.STRIPE_WEBHOOK_SECRET) return true;
2171
+ if (!signature || !rawBody) return false;
2172
+
2173
+ // Stripe signature format: t=<timestamp>,v1=<hmac>,...
2174
+ const parts = { v1: [] };
2175
+ for (const part of signature.split(',')) {
2176
+ const [k, v] = part.split('=');
2177
+ if (!k || !v) continue;
2178
+ if (k === 'v1') {
2179
+ parts.v1.push(v);
2180
+ continue;
2181
+ }
2182
+ parts[k] = v;
2183
+ }
2184
+
2185
+ if (!parts.t || !Array.isArray(parts.v1) || parts.v1.length === 0) return false;
2186
+
2187
+ // Timestamp tolerance: +/- 5 minutes
2188
+ const timestamp = parseInt(parts.t, 10);
2189
+ const now = Math.floor(Date.now() / 1000);
2190
+ if (isNaN(timestamp) || Math.abs(now - timestamp) > 300) return false;
2191
+
2192
+ const payload = `${parts.t}.${typeof rawBody === 'string' ? rawBody : rawBody.toString('utf-8')}`;
2193
+ const expected = crypto.createHmac('sha256', CONFIG.STRIPE_WEBHOOK_SECRET).update(payload).digest('hex');
2194
+
2195
+ return parts.v1.some((candidate) => safeCompareHex(expected, candidate));
2196
+ }
2197
+
2198
+ async function handleWebhook(rawBody, signature) {
2199
+ if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
2200
+ let event;
2201
+ try {
2202
+ const stripe = getStripeClient();
2203
+ event = stripe.webhooks.constructEvent(rawBody, signature, CONFIG.STRIPE_WEBHOOK_SECRET);
2204
+ } catch (err) {
2205
+ return { handled: false, reason: 'invalid_signature', error: err.message };
2206
+ }
2207
+
2208
+ switch (event.type) {
2209
+ case 'checkout.session.completed': {
2210
+ const session = event.data.object;
2211
+ const customerId = session.customer;
2212
+ const installId = session.metadata?.installId;
2213
+ const traceId = session.metadata?.traceId || null;
2214
+ const credits = session.metadata?.credits ? parseInt(session.metadata.credits, 10) : null;
2215
+ const packId = session.metadata?.packId || null;
2216
+
2217
+ const attribution = extractAttribution(session.metadata);
2218
+ const result = provisionApiKey(customerId, {
2219
+ installId,
2220
+ credits,
2221
+ source: 'stripe_webhook_checkout_completed'
2222
+ });
2223
+ const funnelRecord = {
2224
+ stage: 'paid',
2225
+ event: 'stripe_checkout_completed',
2226
+ evidence: session.id,
2227
+ metadata: {
2228
+ customerId,
2229
+ sessionId: session.id,
2230
+ traceId,
2231
+ packId,
2232
+ ...extractJourneyFields(session.metadata),
2233
+ ...attribution,
2234
+ },
2235
+ };
2236
+ if (!hasFunnelEventMatch(loadFunnelLedger(), funnelRecord)) {
2237
+ appendFunnelEvent({
2238
+ stage: 'paid',
2239
+ event: 'stripe_checkout_completed',
2240
+ installId,
2241
+ traceId,
2242
+ evidence: session.id,
2243
+ metadata: funnelRecord.metadata,
2244
+ });
2245
+ }
2246
+ // Write checkout_paid_confirmed event with amount/currency for funnel analytics
2247
+ appendFunnelEvent({
2248
+ stage: 'paid',
2249
+ event: 'checkout_paid_confirmed',
2250
+ installId,
2251
+ traceId,
2252
+ evidence: session.id,
2253
+ metadata: {
2254
+ source: 'stripe_webhook_checkout_completed',
2255
+ amount: session.amount_total,
2256
+ currency: session.currency,
2257
+ customerId,
2258
+ ...funnelRecord.metadata,
2259
+ },
2260
+ });
2261
+ const revenueRecord = {
2262
+ provider: 'stripe',
2263
+ event: 'stripe_checkout_completed',
2264
+ status: 'paid',
2265
+ customerId,
2266
+ orderId: session.id,
2267
+ metadata: {
2268
+ ...extractJourneyFields(session.metadata),
2269
+ sessionId: session.id,
2270
+ mode: session.mode || null,
2271
+ paymentStatus: session.payment_status || null,
2272
+ packId,
2273
+ },
2274
+ };
2275
+ if (!hasRevenueEventMatch(loadRevenueLedger(), revenueRecord)) {
2276
+ appendRevenueEvent({
2277
+ ...revenueRecord,
2278
+ installId,
2279
+ traceId,
2280
+ evidence: session.id,
2281
+ amountCents: session.amount_total,
2282
+ currency: session.currency,
2283
+ amountKnown: session.amount_total !== undefined && session.amount_total !== null,
2284
+ recurringInterval: session.mode === 'subscription' ? 'month' : null,
2285
+ attribution,
2286
+ });
2287
+ }
2288
+ return { handled: true, action: 'provisioned_api_key', result };
2289
+ }
2290
+ case 'customer.subscription.deleted': {
2291
+ const sub = event.data.object;
2292
+ return { handled: true, action: 'disabled_customer_keys', result: disableCustomerKeys(sub.customer) };
2293
+ }
2294
+ default: return { handled: false, reason: `unhandled_event_type:${event.type}` };
2295
+ }
2296
+ }
2297
+
2298
+ function verifyGithubWebhookSignature(rawBody, signature) {
2299
+ if (!CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET) return true;
2300
+ if (!signature || !rawBody) return false;
2301
+ const expected = crypto.createHmac('sha256', CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET).update(rawBody).digest('hex');
2302
+ const digest = Buffer.from(`sha256=${expected}`, 'utf8');
2303
+ const checksum = Buffer.from(signature, 'utf8');
2304
+ return checksum.length === digest.length && crypto.timingSafeEqual(digest, checksum);
2305
+ }
2306
+
2307
+ function buildGithubMarketplaceRevenueMetadata(marketplacePurchase = {}, marketplaceOrderId, planPricing = {}) {
2308
+ const plan = marketplacePurchase && typeof marketplacePurchase.plan === 'object'
2309
+ ? marketplacePurchase.plan
2310
+ : {};
2311
+ return {
2312
+ accountId: normalizeText(marketplacePurchase.account && marketplacePurchase.account.id),
2313
+ accountType: normalizeText(marketplacePurchase.account && marketplacePurchase.account.type),
2314
+ planId: normalizeText(plan.id),
2315
+ planName: normalizeText(plan.name),
2316
+ marketplaceOrderId: normalizeText(marketplaceOrderId),
2317
+ billingCycle: normalizeText(marketplacePurchase.billing_cycle ?? marketplacePurchase.billingCycle),
2318
+ unitCount: normalizeInteger(marketplacePurchase.unit_count ?? marketplacePurchase.unitCount),
2319
+ priceModel: normalizeText(plan.price_model ?? plan.priceModel),
2320
+ monthlyPriceInCents: normalizeInteger(plan.monthly_price_in_cents ?? plan.monthlyPriceInCents),
2321
+ yearlyPriceInCents: normalizeInteger(plan.yearly_price_in_cents ?? plan.yearlyPriceInCents),
2322
+ githubMarketplaceAmountSource: normalizeText(planPricing.pricingSource),
2323
+ };
2324
+ }
2325
+
2326
+ function handleGithubWebhook(event) {
2327
+ if (!event) return { handled: false, reason: 'missing_payload_data' };
2328
+ const { action, marketplace_purchase: mp } = event;
2329
+ if (!action || !mp || !mp.account?.id) return { handled: false, reason: 'missing_payload_data' };
2330
+ const customerId = `github_${String(mp.account.type).toLowerCase()}_${mp.account.id}`;
2331
+ const marketplaceOrderId = normalizeText(mp.id) || `github_marketplace_${String(mp.account.id)}_${String(mp.plan?.id || 'unknown')}`;
2332
+ const planPricing = resolveGithubPlanPricing(mp.plan?.id, mp);
2333
+ const githubMetadata = buildGithubMarketplaceRevenueMetadata(mp, marketplaceOrderId, planPricing);
2334
+ switch (action) {
2335
+ case 'purchased': {
2336
+ const result = provisionApiKey(customerId, { source: 'github_marketplace_purchased' });
2337
+ const funnelRecord = {
2338
+ stage: 'paid',
2339
+ event: 'github_marketplace_purchased',
2340
+ evidence: marketplaceOrderId,
2341
+ metadata: {
2342
+ provider: 'github_marketplace',
2343
+ customerId,
2344
+ source: 'github_marketplace',
2345
+ ...githubMetadata,
2346
+ },
2347
+ };
2348
+ if (!hasFunnelEventMatch(loadFunnelLedger(), funnelRecord)) {
2349
+ appendFunnelEvent(funnelRecord);
2350
+ }
2351
+ const revenueRecord = {
2352
+ provider: 'github_marketplace',
2353
+ event: 'github_marketplace_purchased',
2354
+ status: 'paid',
2355
+ customerId,
2356
+ orderId: marketplaceOrderId,
2357
+ metadata: githubMetadata,
2358
+ };
2359
+ if (!hasRevenueEventMatch(loadRevenueLedger(), revenueRecord)) {
2360
+ appendRevenueEvent({
2361
+ ...revenueRecord,
2362
+ evidence: marketplaceOrderId,
2363
+ amountCents: planPricing.amountCents,
2364
+ currency: planPricing.currency,
2365
+ amountKnown: planPricing.amountKnown,
2366
+ recurringInterval: planPricing.recurringInterval,
2367
+ attribution: { source: 'github_marketplace' },
2368
+ metadata: githubMetadata,
2369
+ });
2370
+ }
2371
+ return { handled: true, action: 'provisioned_api_key', result };
2372
+ }
2373
+ case 'cancelled':
2374
+ if (!hasRevenueEventMatch(loadRevenueLedger(), {
2375
+ provider: 'github_marketplace',
2376
+ event: 'github_marketplace_cancelled',
2377
+ status: 'cancelled',
2378
+ customerId,
2379
+ orderId: marketplaceOrderId,
2380
+ metadata: { marketplaceOrderId },
2381
+ })) {
2382
+ appendRevenueEvent({
2383
+ provider: 'github_marketplace',
2384
+ event: 'github_marketplace_cancelled',
2385
+ status: 'cancelled',
2386
+ customerId,
2387
+ orderId: marketplaceOrderId,
2388
+ evidence: marketplaceOrderId,
2389
+ amountCents: planPricing.amountCents,
2390
+ currency: planPricing.currency,
2391
+ amountKnown: planPricing.amountKnown,
2392
+ recurringInterval: planPricing.recurringInterval,
2393
+ attribution: { source: 'github_marketplace' },
2394
+ metadata: githubMetadata,
2395
+ });
2396
+ }
2397
+ return { handled: true, action: 'disabled_customer_keys', result: disableCustomerKeys(customerId) };
2398
+ case 'changed': {
2399
+ if (!hasRevenueEventMatch(loadRevenueLedger(), {
2400
+ provider: 'github_marketplace',
2401
+ event: 'github_marketplace_changed',
2402
+ status: 'changed',
2403
+ customerId,
2404
+ orderId: marketplaceOrderId,
2405
+ metadata: { marketplaceOrderId },
2406
+ })) {
2407
+ appendRevenueEvent({
2408
+ provider: 'github_marketplace',
2409
+ event: 'github_marketplace_changed',
2410
+ status: 'changed',
2411
+ customerId,
2412
+ orderId: marketplaceOrderId,
2413
+ evidence: marketplaceOrderId,
2414
+ amountCents: planPricing.amountCents,
2415
+ currency: planPricing.currency,
2416
+ amountKnown: planPricing.amountKnown,
2417
+ recurringInterval: planPricing.recurringInterval,
2418
+ attribution: { source: 'github_marketplace' },
2419
+ metadata: githubMetadata,
2420
+ });
2421
+ }
2422
+ return { handled: true, action: 'plan_changed', result: provisionApiKey(customerId, { source: 'github_marketplace_changed' }) };
2423
+ }
2424
+ default: return { handled: false, reason: `unhandled_action:${action}` };
2425
+ }
2426
+ }
2427
+
2428
+ module.exports = {
2429
+ CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
2430
+ _buildCheckoutSessionPayload: buildCheckoutSessionPayload,
2431
+ _resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
2432
+ _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
2433
+ _FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
2434
+ _REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
2435
+ _LOCAL_CHECKOUT_SESSIONS_PATH: () => CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH,
2436
+ _LOCAL_MODE: () => LOCAL_MODE(),
2437
+ _withTimeout: withTimeout,
2438
+ };