@coursebuilder/analytics 1.1.0 → 1.1.2-canary.0.0c9beb2ae

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.
@@ -3,18 +3,89 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/providers/database.ts
5
5
  import { and, count, desc, eq, gt, gte, inArray, lte, sql, sum } from "drizzle-orm";
6
+
7
+ // src/providers/attribution-recovery.ts
8
+ var SHORTLINK_RECOVERY_LANE = "recovered_from_shortlink_attribution_table";
9
+
10
+ // src/providers/database.ts
6
11
  function createDatabaseProvider(db, schema) {
7
- const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick } = schema;
12
+ const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick, questionResponse, contactEvent, sideEffectIntent } = schema;
8
13
  const PAID_STATUSES = [
9
14
  "Valid",
10
15
  "Restricted"
11
16
  ];
12
- function paidPurchase() {
17
+ function commerceRecord() {
13
18
  return inArray(purchases.status, [
14
19
  ...PAID_STATUSES
15
20
  ]);
16
21
  }
22
+ __name(commerceRecord, "commerceRecord");
23
+ function paidPurchase() {
24
+ return and(commerceRecord(), gt(purchases.totalAmount, 0));
25
+ }
17
26
  __name(paidPurchase, "paidPurchase");
27
+ function accessGrant() {
28
+ return and(commerceRecord(), sql`${purchases.totalAmount} = 0`);
29
+ }
30
+ __name(accessGrant, "accessGrant");
31
+ function backfillFreeUpgrade() {
32
+ return and(accessGrant(), sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'`);
33
+ }
34
+ __name(backfillFreeUpgrade, "backfillFreeUpgrade");
35
+ const syntheticSignalSql = sql`(
36
+ JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.synthetic')) = 'true'
37
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.synthetic')) = 'true'
38
+ OR JSON_SEARCH(JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds'), 'one', 'TEST_%') IS NOT NULL
39
+ )`;
40
+ function syntheticPurchase() {
41
+ return and(commerceRecord(), syntheticSignalSql);
42
+ }
43
+ __name(syntheticPurchase, "syntheticPurchase");
44
+ const purchaseFieldAttributionSignalSql = sql`(
45
+ JSON_EXTRACT(${purchases.fields}, '$.attribution') IS NOT NULL
46
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
47
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
48
+ )`;
49
+ const exactShortlinkPurchaseAttributionSignalSql = sql`EXISTS (
50
+ SELECT 1
51
+ FROM ${shortlinkAttribution}
52
+ WHERE ${shortlinkAttribution.type} = 'purchase'
53
+ AND JSON_VALID(${shortlinkAttribution.metadata})
54
+ AND JSON_UNQUOTE(JSON_EXTRACT(${shortlinkAttribution.metadata}, '$.purchaseId')) = ${purchases.id}
55
+ )`;
56
+ const shortlinkRecoveredAttributionSignalSql = sql`(
57
+ NOT ${purchaseFieldAttributionSignalSql}
58
+ AND ${exactShortlinkPurchaseAttributionSignalSql}
59
+ )`;
60
+ const attributionSignalSql = sql`(
61
+ ${purchaseFieldAttributionSignalSql}
62
+ OR ${exactShortlinkPurchaseAttributionSignalSql}
63
+ )`;
64
+ const strongAttributionQualitySql = sql`(
65
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL
66
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
67
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink') IS NOT NULL
68
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
69
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
70
+ )`;
71
+ const mediumAttributionQualitySql = sql`(
72
+ NOT ${strongAttributionQualitySql}
73
+ AND ${exactShortlinkPurchaseAttributionSignalSql}
74
+ )`;
75
+ const weakAttributionQualitySql = sql`(
76
+ NOT ${strongAttributionQualitySql}
77
+ AND NOT ${exactShortlinkPurchaseAttributionSignalSql}
78
+ AND (
79
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
80
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
81
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL
82
+ )
83
+ )`;
84
+ const unknownAttributionQualitySql = sql`(
85
+ NOT ${strongAttributionQualitySql}
86
+ AND NOT ${mediumAttributionQualitySql}
87
+ AND NOT ${weakAttributionQualitySql}
88
+ )`;
18
89
  function rangeToDate(range) {
19
90
  if (range === "all")
20
91
  return null;
@@ -255,21 +326,80 @@ function createDatabaseProvider(db, schema) {
255
326
  });
256
327
  }
257
328
  __name(getShortlinkPerformance, "getShortlinkPerformance");
258
- async function getRevenueBySource(range = "30d") {
329
+ async function getRevenueBySource(range = "30d", filters = {}) {
259
330
  const since = rangeToDate(range);
260
331
  const conditions = [
261
- paidPurchase()
332
+ commerceRecord()
262
333
  ];
263
334
  if (since)
264
335
  conditions.push(gte(purchases.createdAt, since));
336
+ if (filters.productId)
337
+ conditions.push(eq(purchases.productId, filters.productId));
338
+ const kindExpr = sql`CASE
339
+ WHEN ${syntheticSignalSql} THEN 'synthetic'
340
+ WHEN ${purchases.totalAmount} = 0 THEN 'access_grant'
341
+ WHEN ${purchases.totalAmount} > 0 THEN 'paid_conversion'
342
+ ELSE 'unknown'
343
+ END`;
344
+ const sourceExpr = sql`CASE
345
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
346
+ THEN 'internal'
347
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN ${SHORTLINK_RECOVERY_LANE}
348
+ ELSE COALESCE(
349
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.source')), 'null'),
350
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.source')), 'null'),
351
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')), 'null'),
352
+ CASE
353
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
354
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'self_reported'
355
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'ga4'
356
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'ga4'
357
+ ELSE NULL
358
+ END
359
+ )
360
+ END`;
361
+ const mediumExpr = sql`CASE
362
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
363
+ THEN 'free_upgrade'
364
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN 'shortlink'
365
+ ELSE COALESCE(
366
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.medium')), 'null'),
367
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.medium')), 'null'),
368
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium')), 'null'),
369
+ CASE
370
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
371
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'checkout_survey'
372
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'client_id'
373
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'client_id'
374
+ ELSE NULL
375
+ END
376
+ )
377
+ END`;
378
+ const campaignExpr = sql`CASE
379
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
380
+ THEN COALESCE(
381
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.migrationBatchId')), 'null'),
382
+ 'free_upgrade'
383
+ )
384
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN NULL
385
+ ELSE COALESCE(
386
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.campaign')), 'null'),
387
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.campaign')), 'null'),
388
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign')), 'null'),
389
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug')), 'null'),
390
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource')), 'null')
391
+ )
392
+ END`;
265
393
  const rows = await db.select({
266
- source: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`.as("source"),
267
- medium: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`.as("medium"),
268
- campaign: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`.as("campaign"),
394
+ kind: kindExpr.as("kind"),
395
+ source: sourceExpr.as("source"),
396
+ medium: mediumExpr.as("medium"),
397
+ campaign: campaignExpr.as("campaign"),
269
398
  revenue: sum(purchases.totalAmount),
270
399
  count: count()
271
- }).from(purchases).where(and(...conditions)).groupBy(sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`, sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`, sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`).orderBy(desc(sum(purchases.totalAmount)));
400
+ }).from(purchases).where(and(...conditions)).groupBy(sql`1`, sql`2`, sql`3`, sql`4`).orderBy(desc(sum(purchases.totalAmount)), desc(count()));
272
401
  return rows.map((r) => ({
402
+ kind: r.kind ?? "unknown",
273
403
  source: r.source ?? null,
274
404
  medium: r.medium ?? null,
275
405
  campaign: r.campaign ?? null,
@@ -296,10 +426,7 @@ function createDatabaseProvider(db, schema) {
296
426
  }).from(purchases).where(and(...purchaseConditions));
297
427
  const [attributedCount] = await db.select({
298
428
  total: count()
299
- }).from(purchases).where(and(...purchaseConditions, sql`(
300
- JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
301
- OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
302
- )`));
429
+ }).from(purchases).where(and(...purchaseConditions, attributionSignalSql));
303
430
  const totalSignups = userCount?.total ?? 0;
304
431
  const totalPurchases = purchaseCount?.total ?? 0;
305
432
  const attributedPurchases = attributedCount?.total ?? 0;
@@ -312,33 +439,180 @@ function createDatabaseProvider(db, schema) {
312
439
  };
313
440
  }
314
441
  __name(getConversionFunnel, "getConversionFunnel");
315
- async function getAttributedRevenueSummary(range = "30d") {
442
+ async function getCommerceLaneSummary(range = "30d", filters = {}) {
443
+ const since = rangeToDate(range);
444
+ const conditions = [
445
+ commerceRecord()
446
+ ];
447
+ if (since)
448
+ conditions.push(gte(purchases.createdAt, since));
449
+ if (filters.productId)
450
+ conditions.push(eq(purchases.productId, filters.productId));
451
+ const [commerce] = await db.select({
452
+ count: count()
453
+ }).from(purchases).where(and(...conditions));
454
+ const [paid] = await db.select({
455
+ count: count(),
456
+ revenue: sum(purchases.totalAmount)
457
+ }).from(purchases).where(and(...conditions, paidPurchase()));
458
+ const [grants] = await db.select({
459
+ count: count(),
460
+ revenue: sum(purchases.totalAmount)
461
+ }).from(purchases).where(and(...conditions, accessGrant()));
462
+ const [freeUpgrades] = await db.select({
463
+ count: count()
464
+ }).from(purchases).where(and(...conditions, backfillFreeUpgrade()));
465
+ const [synthetic] = await db.select({
466
+ count: count()
467
+ }).from(purchases).where(and(...conditions, syntheticPurchase()));
468
+ const reasonRows = await db.select({
469
+ reason: sql`COALESCE(
470
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
471
+ 'access_grant'
472
+ )`.as("reason"),
473
+ count: count()
474
+ }).from(purchases).where(and(...conditions, accessGrant())).groupBy(sql`COALESCE(
475
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
476
+ 'access_grant'
477
+ )`);
478
+ return {
479
+ commerceRecords: commerce?.count ?? 0,
480
+ paidPurchases: paid?.count ?? 0,
481
+ paidRevenue: Number(paid?.revenue ?? 0),
482
+ accessGrants: grants?.count ?? 0,
483
+ accessGrantRevenue: Number(grants?.revenue ?? 0),
484
+ freeUpgrades: freeUpgrades?.count ?? 0,
485
+ syntheticPurchases: synthetic?.count ?? 0,
486
+ byAccessGrantReason: Object.fromEntries(reasonRows.map((row) => [
487
+ row.reason,
488
+ row.count
489
+ ]))
490
+ };
491
+ }
492
+ __name(getCommerceLaneSummary, "getCommerceLaneSummary");
493
+ async function getAttributedRevenueSummary(range = "30d", filters = {}) {
316
494
  const since = rangeToDate(range);
317
495
  const conditions = [
318
496
  paidPurchase()
319
497
  ];
320
498
  if (since)
321
499
  conditions.push(gte(purchases.createdAt, since));
500
+ if (filters.productId)
501
+ conditions.push(eq(purchases.productId, filters.productId));
322
502
  const [totals] = await db.select({
323
503
  total: sum(purchases.totalAmount),
324
504
  count: count()
325
505
  }).from(purchases).where(and(...conditions));
326
506
  const [attributed] = await db.select({
327
- total: sum(purchases.totalAmount)
328
- }).from(purchases).where(and(...conditions, sql`(
329
- JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
330
- OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
331
- )`));
507
+ total: sum(purchases.totalAmount),
508
+ count: count()
509
+ }).from(purchases).where(and(...conditions, attributionSignalSql));
510
+ const [purchaseFieldAttributed] = await db.select({
511
+ total: sum(purchases.totalAmount),
512
+ count: count()
513
+ }).from(purchases).where(and(...conditions, purchaseFieldAttributionSignalSql));
514
+ const [recoveredFromShortlinkAttributionTable] = await db.select({
515
+ total: sum(purchases.totalAmount),
516
+ count: count()
517
+ }).from(purchases).where(and(...conditions, shortlinkRecoveredAttributionSignalSql));
518
+ const signalConditions = since ? [
519
+ commerceRecord(),
520
+ gte(purchases.createdAt, since)
521
+ ] : [
522
+ commerceRecord()
523
+ ];
524
+ if (filters.productId) {
525
+ signalConditions.push(eq(purchases.productId, filters.productId));
526
+ }
527
+ const signalCount = /* @__PURE__ */ __name(async (condition) => {
528
+ const [row] = await db.select({
529
+ count: count()
530
+ }).from(purchases).where(and(...signalConditions, condition));
531
+ return row?.count ?? 0;
532
+ }, "signalCount");
533
+ const qualityLane = /* @__PURE__ */ __name(async (lane, condition, notes) => {
534
+ const [row] = await db.select({
535
+ total: sum(purchases.totalAmount),
536
+ count: count()
537
+ }).from(purchases).where(and(...conditions, condition));
538
+ const revenue = Number(row?.total ?? 0);
539
+ return {
540
+ lane,
541
+ purchases: row?.count ?? 0,
542
+ revenue,
543
+ revenuePercent: totalRevenue > 0 ? revenue / totalRevenue : 0,
544
+ notes
545
+ };
546
+ }, "qualityLane");
547
+ const [shortlinkCount, utmCount, clickIdCount, gaClientIdCount, selfReportedSourceCount, recoveredFromShortlinkAttributionTableCount] = await Promise.all([
548
+ signalCount(sql`(
549
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL
550
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
551
+ )`),
552
+ signalCount(sql`(
553
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
554
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
555
+ )`),
556
+ signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL`),
557
+ signalCount(sql`(
558
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
559
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
560
+ )`),
561
+ signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL`),
562
+ signalCount(shortlinkRecoveredAttributionSignalSql)
563
+ ]);
564
+ const commerce = await getCommerceLaneSummary(range, filters);
332
565
  const totalRevenue = Number(totals?.total ?? 0);
333
566
  const attributedRevenue = Number(attributed?.total ?? 0);
567
+ const purchaseFieldAttributedRevenue = Number(purchaseFieldAttributed?.total ?? 0);
568
+ const recoveredFromShortlinkAttributionTableRevenue = Number(recoveredFromShortlinkAttributionTable?.total ?? 0);
334
569
  const unattributedRevenue = totalRevenue - attributedRevenue;
335
- const totalPurchases = totals?.count ?? 0;
570
+ const paidPurchases = totals?.count ?? 0;
571
+ const [strongQuality, mediumQuality, weakQuality, unknownQuality] = await Promise.all([
572
+ qualityLane("strong", strongAttributionQualitySql, [
573
+ "Purchase-field UTM, shortlink, or paid click evidence."
574
+ ]),
575
+ qualityLane("medium", mediumAttributionQualitySql, [
576
+ "Recovered from exact shortlink attribution table purchase match."
577
+ ]),
578
+ qualityLane("weak", weakAttributionQualitySql, [
579
+ "GA client ID or self-reported source without stronger campaign evidence."
580
+ ]),
581
+ qualityLane("unknown", unknownAttributionQualitySql, [
582
+ "No usable attribution evidence for paid revenue."
583
+ ])
584
+ ]);
336
585
  return {
337
586
  totalRevenue,
338
587
  attributedRevenue,
339
588
  unattributedRevenue,
340
589
  attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
341
- totalPurchases
590
+ totalPurchases: commerce.commerceRecords,
591
+ paidPurchases,
592
+ purchaseFieldAttributedPurchases: purchaseFieldAttributed?.count ?? 0,
593
+ purchaseFieldAttributedRevenue,
594
+ recoveredFromShortlinkAttributionTablePurchases: recoveredFromShortlinkAttributionTable?.count ?? 0,
595
+ recoveredFromShortlinkAttributionTableRevenue,
596
+ commerceRecords: commerce.commerceRecords,
597
+ accessGrants: commerce.accessGrants,
598
+ freeUpgrades: commerce.freeUpgrades,
599
+ syntheticPurchases: commerce.syntheticPurchases,
600
+ commerce,
601
+ signals: {
602
+ shortlink: shortlinkCount,
603
+ utm: utmCount,
604
+ paidClickId: clickIdCount,
605
+ gaClientId: gaClientIdCount,
606
+ selfReportedSource: selfReportedSourceCount,
607
+ recoveredFromShortlinkAttributionTable: recoveredFromShortlinkAttributionTableCount,
608
+ internalFreeUpgrade: commerce.freeUpgrades
609
+ },
610
+ quality: {
611
+ strong: strongQuality,
612
+ medium: mediumQuality,
613
+ weak: weakQuality,
614
+ unknown: unknownQuality
615
+ }
342
616
  };
343
617
  }
344
618
  __name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
@@ -365,6 +639,419 @@ function createDatabaseProvider(db, schema) {
365
639
  }));
366
640
  }
367
641
  __name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
642
+ function parseJsonRecord(value) {
643
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
644
+ }
645
+ __name(parseJsonRecord, "parseJsonRecord");
646
+ function hasRecordValue(value) {
647
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.length > 0);
648
+ }
649
+ __name(hasRecordValue, "hasRecordValue");
650
+ function hasSyntheticClickId(value) {
651
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.startsWith("TEST_"));
652
+ }
653
+ __name(hasSyntheticClickId, "hasSyntheticClickId");
654
+ async function getCheckoutAttributionReceipt(opts) {
655
+ if (!opts.purchaseId) {
656
+ return {
657
+ purchase: null,
658
+ checks: {
659
+ purchaseFound: false,
660
+ attributionSnapshot: false,
661
+ utm: false,
662
+ actualUtm: false,
663
+ clickId: false,
664
+ synthetic: false,
665
+ shortlink: false,
666
+ selfReportedSource: false,
667
+ gaClientId: false
668
+ },
669
+ attribution: null,
670
+ legacy: {},
671
+ fieldKeys: []
672
+ };
673
+ }
674
+ const [row] = await db.select({
675
+ id: purchases.id,
676
+ createdAt: purchases.createdAt,
677
+ productId: purchases.productId,
678
+ productName: products.name,
679
+ totalAmount: purchases.totalAmount,
680
+ status: purchases.status,
681
+ country: purchases.country,
682
+ fields: purchases.fields
683
+ }).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
684
+ if (!row) {
685
+ return {
686
+ purchase: null,
687
+ checks: {
688
+ purchaseFound: false,
689
+ attributionSnapshot: false,
690
+ utm: false,
691
+ actualUtm: false,
692
+ clickId: false,
693
+ synthetic: false,
694
+ shortlink: false,
695
+ selfReportedSource: false,
696
+ gaClientId: false
697
+ },
698
+ attribution: null,
699
+ legacy: {},
700
+ fieldKeys: []
701
+ };
702
+ }
703
+ const fields = parseJsonRecord(row.fields);
704
+ const attribution = parseJsonRecord(fields.attribution);
705
+ const utm = parseJsonRecord(attribution.utm);
706
+ const clickIds = parseJsonRecord(attribution.clickIds);
707
+ const shortlinkSnapshot = parseJsonRecord(attribution.shortlink);
708
+ const ga = parseJsonRecord(attribution.ga);
709
+ const legacy = Object.fromEntries([
710
+ "utmSource",
711
+ "utmMedium",
712
+ "utmCampaign",
713
+ "gaClientId",
714
+ "shortlinkRef",
715
+ "landingPath",
716
+ "referrer",
717
+ "ltUtmSource",
718
+ "ltUtmMedium",
719
+ "ltUtmCampaign"
720
+ ].map((key) => [
721
+ key,
722
+ fields[key]
723
+ ]).filter(([, value]) => value !== void 0 && value !== null));
724
+ const hasActualUtm = Boolean(utm.source || utm.medium || utm.campaign || fields.utmSource);
725
+ const synthetic = Boolean(attribution.synthetic || fields.synthetic || hasSyntheticClickId(clickIds));
726
+ return {
727
+ purchase: {
728
+ id: row.id,
729
+ createdAt: row.createdAt,
730
+ productId: row.productId,
731
+ productName: row.productName ?? null,
732
+ totalAmount: Number(row.totalAmount ?? 0),
733
+ status: row.status,
734
+ country: row.country
735
+ },
736
+ checks: {
737
+ purchaseFound: true,
738
+ attributionSnapshot: Object.keys(attribution).length > 0,
739
+ utm: hasActualUtm,
740
+ actualUtm: hasActualUtm,
741
+ clickId: hasRecordValue(clickIds),
742
+ synthetic,
743
+ shortlink: Boolean(shortlinkSnapshot.slug || fields.shortlinkRef),
744
+ selfReportedSource: Boolean(attribution.selfReportedSource),
745
+ gaClientId: Boolean(ga.clientId || fields.gaClientId)
746
+ },
747
+ attribution: Object.keys(attribution).length > 0 ? attribution : null,
748
+ legacy,
749
+ fieldKeys: Object.keys(fields).sort()
750
+ };
751
+ }
752
+ __name(getCheckoutAttributionReceipt, "getCheckoutAttributionReceipt");
753
+ async function getCheckoutSurveyFallbackReport(range = "30d", limit = 20, filters = {}) {
754
+ const label = "recovered_from_checkout_survey_response";
755
+ if (!questionResponse) {
756
+ return {
757
+ label,
758
+ productId: filters.productId ?? null,
759
+ totalDarkPurchases: 0,
760
+ totalDarkRevenue: 0,
761
+ exactPurchaseLinkedPurchases: 0,
762
+ exactPurchaseLinkedRevenue: 0,
763
+ userLinkedPurchases: 0,
764
+ userLinkedRevenue: 0,
765
+ byAnswer: [],
766
+ notes: [
767
+ "questionResponse table was not provided to this provider"
768
+ ]
769
+ };
770
+ }
771
+ const since = rangeToDate(range);
772
+ const purchaseConditions = [
773
+ paidPurchase(),
774
+ sql`NOT ${attributionSignalSql}`
775
+ ];
776
+ if (since)
777
+ purchaseConditions.push(gte(purchases.createdAt, since));
778
+ if (filters.productId) {
779
+ purchaseConditions.push(eq(purchases.productId, filters.productId));
780
+ }
781
+ const darkRows = await db.select({
782
+ id: purchases.id,
783
+ userId: purchases.userId,
784
+ revenue: purchases.totalAmount
785
+ }).from(purchases).where(and(...purchaseConditions));
786
+ const totalDarkRevenue = darkRows.reduce((total, row) => total + Number(row.revenue ?? 0), 0);
787
+ if (darkRows.length === 0) {
788
+ return {
789
+ label,
790
+ productId: filters.productId ?? null,
791
+ totalDarkPurchases: 0,
792
+ totalDarkRevenue: 0,
793
+ exactPurchaseLinkedPurchases: 0,
794
+ exactPurchaseLinkedRevenue: 0,
795
+ userLinkedPurchases: 0,
796
+ userLinkedRevenue: 0,
797
+ byAnswer: [],
798
+ notes: [
799
+ "No dark paid purchases matched the filters"
800
+ ]
801
+ };
802
+ }
803
+ const darkById = new Map(darkRows.map((row) => [
804
+ row.id,
805
+ row
806
+ ]));
807
+ const darkByUserId = /* @__PURE__ */ new Map();
808
+ for (const row of darkRows) {
809
+ if (!row.userId)
810
+ continue;
811
+ const bucket = darkByUserId.get(row.userId) ?? [];
812
+ bucket.push(row);
813
+ darkByUserId.set(row.userId, bucket);
814
+ }
815
+ const responseConditions = [];
816
+ if (since)
817
+ responseConditions.push(gte(questionResponse.createdAt, since));
818
+ const darkIds = [
819
+ ...darkById.keys()
820
+ ];
821
+ const darkUserIds = [
822
+ ...darkByUserId.keys()
823
+ ];
824
+ const exactPurchaseCondition = sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId')) IN (${sql.join(darkIds.map((id) => sql`${id}`), sql`, `)})`;
825
+ const userCondition = darkUserIds.length > 0 ? sql`${questionResponse.userId} IN (${sql.join(darkUserIds.map((id) => sql`${id}`), sql`, `)})` : sql`FALSE`;
826
+ responseConditions.push(sql`(${exactPurchaseCondition} OR ${userCondition})`);
827
+ const responseRows = await db.select({
828
+ purchaseId: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId'))`,
829
+ answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
830
+ userId: questionResponse.userId,
831
+ createdAt: questionResponse.createdAt
832
+ }).from(questionResponse).where(and(...responseConditions));
833
+ const exactByPurchase = /* @__PURE__ */ new Map();
834
+ const latestByUser = /* @__PURE__ */ new Map();
835
+ for (const row of responseRows) {
836
+ if (row.purchaseId && darkById.has(row.purchaseId)) {
837
+ exactByPurchase.set(row.purchaseId, row);
838
+ continue;
839
+ }
840
+ if (!row.userId)
841
+ continue;
842
+ const current = latestByUser.get(row.userId);
843
+ const rowTime = new Date(row.createdAt ?? 0).getTime();
844
+ const currentTime = current ? new Date(current.createdAt ?? 0).getTime() : -1;
845
+ if (!current || rowTime >= currentTime)
846
+ latestByUser.set(row.userId, row);
847
+ }
848
+ const classified = /* @__PURE__ */ new Map();
849
+ for (const [purchaseId, response] of exactByPurchase) {
850
+ const purchase = darkById.get(purchaseId);
851
+ classified.set(purchaseId, {
852
+ confidence: "exact_purchase_link",
853
+ answer: response.answer || "(no answer)",
854
+ revenue: Number(purchase?.revenue ?? 0)
855
+ });
856
+ }
857
+ for (const row of darkRows) {
858
+ if (classified.has(row.id) || !row.userId)
859
+ continue;
860
+ const response = latestByUser.get(row.userId);
861
+ if (!response)
862
+ continue;
863
+ classified.set(row.id, {
864
+ confidence: "user_linked",
865
+ answer: response.answer || "(no answer)",
866
+ revenue: Number(row.revenue ?? 0)
867
+ });
868
+ }
869
+ const byAnswer = /* @__PURE__ */ new Map();
870
+ let exactPurchaseLinkedPurchases = 0;
871
+ let exactPurchaseLinkedRevenue = 0;
872
+ let userLinkedPurchases = 0;
873
+ let userLinkedRevenue = 0;
874
+ for (const entry of classified.values()) {
875
+ if (entry.confidence === "exact_purchase_link") {
876
+ exactPurchaseLinkedPurchases += 1;
877
+ exactPurchaseLinkedRevenue += entry.revenue;
878
+ } else {
879
+ userLinkedPurchases += 1;
880
+ userLinkedRevenue += entry.revenue;
881
+ }
882
+ const key = `${entry.confidence}:${entry.answer}`;
883
+ const current = byAnswer.get(key) ?? {
884
+ answer: entry.answer,
885
+ confidence: entry.confidence,
886
+ purchases: 0,
887
+ revenue: 0
888
+ };
889
+ current.purchases += 1;
890
+ current.revenue += entry.revenue;
891
+ byAnswer.set(key, current);
892
+ }
893
+ return {
894
+ label,
895
+ productId: filters.productId ?? null,
896
+ totalDarkPurchases: darkRows.length,
897
+ totalDarkRevenue,
898
+ exactPurchaseLinkedPurchases,
899
+ exactPurchaseLinkedRevenue,
900
+ userLinkedPurchases,
901
+ userLinkedRevenue,
902
+ byAnswer: [
903
+ ...byAnswer.values()
904
+ ].sort((a, b) => b.revenue - a.revenue || b.purchases - a.purchases).slice(0, limit),
905
+ notes: [
906
+ "Report-only fallback. No purchase fields are mutated.",
907
+ "Exact purchase-linked responses use QuestionResponse.fields.purchaseId when present.",
908
+ "User-linked responses are lower-confidence and remain report-only."
909
+ ]
910
+ };
911
+ }
912
+ __name(getCheckoutSurveyFallbackReport, "getCheckoutSurveyFallbackReport");
913
+ async function getValuePathSummary(range = "30d") {
914
+ if (!contactEvent || !sideEffectIntent) {
915
+ return {
916
+ contacts: 0,
917
+ events: 0,
918
+ intents: 0,
919
+ completedIntents: 0,
920
+ pendingIntents: 0,
921
+ blockedIntents: 0,
922
+ answerEvents: 0,
923
+ dripEvents: 0,
924
+ enteredEvents: 0,
925
+ participantsWithAnswerClicks: 0,
926
+ participantsWithNoAnswerClicks: 0,
927
+ terminalParticipants: 0,
928
+ answerOptions: [],
929
+ answerSteps: [],
930
+ terminalSteps: [],
931
+ notes: [
932
+ "Contact Event and SideEffectIntent tables are unavailable."
933
+ ]
934
+ };
935
+ }
936
+ const since = rangeToDate(range);
937
+ const eventConditions = [
938
+ sql`${contactEvent.eventType} LIKE 'value-path.%'`
939
+ ];
940
+ const intentConditions = [
941
+ eq(sideEffectIntent.type, "send-value-path-email")
942
+ ];
943
+ if (since) {
944
+ eventConditions.push(gte(contactEvent.occurredAt, since));
945
+ intentConditions.push(gte(sideEffectIntent.createdAt, since));
946
+ }
947
+ const [events, intents] = await Promise.all([
948
+ db.select({
949
+ contactId: contactEvent.contactId,
950
+ eventType: contactEvent.eventType,
951
+ providerEventId: contactEvent.providerEventId,
952
+ occurredAt: contactEvent.occurredAt
953
+ }).from(contactEvent).where(and(...eventConditions)),
954
+ db.select({
955
+ contactId: sideEffectIntent.contactId,
956
+ status: sideEffectIntent.status,
957
+ metadata: sideEffectIntent.metadata,
958
+ createdAt: sideEffectIntent.createdAt
959
+ }).from(sideEffectIntent).where(and(...intentConditions))
960
+ ]);
961
+ const contactIds = /* @__PURE__ */ new Set();
962
+ for (const event of events)
963
+ contactIds.add(event.contactId);
964
+ for (const intent of intents)
965
+ contactIds.add(intent.contactId);
966
+ const answerEvents = events.filter((event) => event.eventType === "value-path.answer-selected");
967
+ const dripEvents = events.filter((event) => event.eventType === "value-path.drip-progressed");
968
+ const enteredEvents = events.filter((event) => event.eventType === "value-path.entered");
969
+ const answersByContact = /* @__PURE__ */ new Map();
970
+ for (const event of answerEvents) {
971
+ answersByContact.set(event.contactId, (answersByContact.get(event.contactId) ?? 0) + 1);
972
+ }
973
+ const answerKeyCounts = /* @__PURE__ */ new Map();
974
+ const answerStepCounts = /* @__PURE__ */ new Map();
975
+ for (const event of answerEvents) {
976
+ const key = parseValuePathAnswerKey(event.providerEventId);
977
+ answerKeyCounts.set(key, (answerKeyCounts.get(key) ?? 0) + 1);
978
+ const step = answerStepFromKey(key);
979
+ answerStepCounts.set(step, (answerStepCounts.get(step) ?? 0) + 1);
980
+ }
981
+ const latestIntentByContact = /* @__PURE__ */ new Map();
982
+ for (const intent of [
983
+ ...intents
984
+ ].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())) {
985
+ latestIntentByContact.set(intent.contactId, intent);
986
+ }
987
+ const terminalCounts = /* @__PURE__ */ new Map();
988
+ for (const intent of latestIntentByContact.values()) {
989
+ const emailResourceId = String(intent.metadata?.emailResourceId ?? "");
990
+ if (!isTerminalValuePathEmail(emailResourceId))
991
+ continue;
992
+ terminalCounts.set(emailResourceId, (terminalCounts.get(emailResourceId) ?? 0) + 1);
993
+ }
994
+ return {
995
+ contacts: contactIds.size,
996
+ events: events.length,
997
+ intents: intents.length,
998
+ completedIntents: intents.filter((intent) => intent.status === "completed").length,
999
+ pendingIntents: intents.filter((intent) => intent.status === "pending").length,
1000
+ blockedIntents: intents.filter((intent) => intent.status === "blocked").length,
1001
+ answerEvents: answerEvents.length,
1002
+ dripEvents: dripEvents.length,
1003
+ enteredEvents: enteredEvents.length,
1004
+ participantsWithAnswerClicks: answersByContact.size,
1005
+ participantsWithNoAnswerClicks: Math.max(0, contactIds.size - answersByContact.size),
1006
+ terminalParticipants: [
1007
+ ...terminalCounts.values()
1008
+ ].reduce((total, count3) => total + count3, 0),
1009
+ answerOptions: [
1010
+ ...answerKeyCounts.entries()
1011
+ ].map(([key, count3]) => ({
1012
+ key,
1013
+ step: answerStepFromKey(key),
1014
+ optionValue: answerOptionFromKey(key),
1015
+ count: count3
1016
+ })).sort((a, b) => b.count - a.count),
1017
+ answerSteps: [
1018
+ ...answerStepCounts.entries()
1019
+ ].map(([step, count3]) => ({
1020
+ step,
1021
+ count: count3
1022
+ })).sort((a, b) => b.count - a.count),
1023
+ terminalSteps: [
1024
+ ...terminalCounts.entries()
1025
+ ].map(([emailResourceId, count3]) => ({
1026
+ emailResourceId,
1027
+ count: count3
1028
+ })).sort((a, b) => b.count - a.count),
1029
+ notes: [
1030
+ "Counts are based on Contact Events and SideEffectIntents.",
1031
+ "Kit-native opens, unsubscribes, complaints, and raw Kit link clicks are not included."
1032
+ ]
1033
+ };
1034
+ }
1035
+ __name(getValuePathSummary, "getValuePathSummary");
1036
+ function parseValuePathAnswerKey(providerEventId) {
1037
+ const match = String(providerEventId ?? "").match(/:answer:(.+)$/);
1038
+ return match?.[1] ?? "unknown";
1039
+ }
1040
+ __name(parseValuePathAnswerKey, "parseValuePathAnswerKey");
1041
+ function answerStepFromKey(key) {
1042
+ const index = key.lastIndexOf(".");
1043
+ return index > -1 ? key.slice(0, index) : key;
1044
+ }
1045
+ __name(answerStepFromKey, "answerStepFromKey");
1046
+ function answerOptionFromKey(key) {
1047
+ const index = key.lastIndexOf(".");
1048
+ return index > -1 ? key.slice(index + 1) : "unknown";
1049
+ }
1050
+ __name(answerOptionFromKey, "answerOptionFromKey");
1051
+ function isTerminalValuePathEmail(emailResourceId) {
1052
+ return /(?:^|\.)(?:email-6|team-email-6)$/.test(emailResourceId);
1053
+ }
1054
+ __name(isTerminalValuePathEmail, "isTerminalValuePathEmail");
368
1055
  async function traceAttribution(opts) {
369
1056
  const events = [];
370
1057
  let userId = null;
@@ -521,8 +1208,12 @@ function createDatabaseProvider(db, schema) {
521
1208
  getShortlinkPerformance,
522
1209
  getRevenueBySource,
523
1210
  getConversionFunnel,
1211
+ getCommerceLaneSummary,
524
1212
  getAttributedRevenueSummary,
525
1213
  getContentPurchaseCorrelation,
1214
+ getCheckoutAttributionReceipt,
1215
+ getCheckoutSurveyFallbackReport,
1216
+ getValuePathSummary,
526
1217
  traceAttribution
527
1218
  };
528
1219
  }
@@ -1202,9 +1893,9 @@ function createSurveyProvider(db, schema) {
1202
1893
  }));
1203
1894
  }
1204
1895
  __name(getSurveyQuestionBreakdown, "getSurveyQuestionBreakdown");
1205
- async function getSurveyResponses(range = "30d", limit = 100) {
1896
+ async function getSurveyResponses(range = "30d", limit = 100, offset = 0) {
1206
1897
  const canonicalRows = await fetchCanonicalRows(range);
1207
- return canonicalRows.slice(0, limit).map((row) => ({
1898
+ return canonicalRows.slice(offset, offset + limit).map((row) => ({
1208
1899
  responseId: row.responseId,
1209
1900
  surveyId: row.surveyId,
1210
1901
  surveyTitle: row.surveyTitle ?? "",