@coursebuilder/analytics 1.1.0 → 1.1.1

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,64 @@ 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
+ )`;
18
64
  function rangeToDate(range) {
19
65
  if (range === "all")
20
66
  return null;
@@ -255,21 +301,80 @@ function createDatabaseProvider(db, schema) {
255
301
  });
256
302
  }
257
303
  __name(getShortlinkPerformance, "getShortlinkPerformance");
258
- async function getRevenueBySource(range = "30d") {
304
+ async function getRevenueBySource(range = "30d", filters = {}) {
259
305
  const since = rangeToDate(range);
260
306
  const conditions = [
261
- paidPurchase()
307
+ commerceRecord()
262
308
  ];
263
309
  if (since)
264
310
  conditions.push(gte(purchases.createdAt, since));
311
+ if (filters.productId)
312
+ conditions.push(eq(purchases.productId, filters.productId));
313
+ const kindExpr = sql`CASE
314
+ WHEN ${syntheticSignalSql} THEN 'synthetic'
315
+ WHEN ${purchases.totalAmount} = 0 THEN 'access_grant'
316
+ WHEN ${purchases.totalAmount} > 0 THEN 'paid_conversion'
317
+ ELSE 'unknown'
318
+ END`;
319
+ const sourceExpr = sql`CASE
320
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
321
+ THEN 'internal'
322
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN ${SHORTLINK_RECOVERY_LANE}
323
+ ELSE COALESCE(
324
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.source')), 'null'),
325
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.source')), 'null'),
326
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')), 'null'),
327
+ CASE
328
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
329
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'self_reported'
330
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'ga4'
331
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'ga4'
332
+ ELSE NULL
333
+ END
334
+ )
335
+ END`;
336
+ const mediumExpr = sql`CASE
337
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
338
+ THEN 'free_upgrade'
339
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN 'shortlink'
340
+ ELSE COALESCE(
341
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.medium')), 'null'),
342
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.medium')), 'null'),
343
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium')), 'null'),
344
+ CASE
345
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
346
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'checkout_survey'
347
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'client_id'
348
+ WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'client_id'
349
+ ELSE NULL
350
+ END
351
+ )
352
+ END`;
353
+ const campaignExpr = sql`CASE
354
+ WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
355
+ THEN COALESCE(
356
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.migrationBatchId')), 'null'),
357
+ 'free_upgrade'
358
+ )
359
+ WHEN ${shortlinkRecoveredAttributionSignalSql} THEN NULL
360
+ ELSE COALESCE(
361
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.campaign')), 'null'),
362
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.campaign')), 'null'),
363
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign')), 'null'),
364
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug')), 'null'),
365
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource')), 'null')
366
+ )
367
+ END`;
265
368
  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"),
369
+ kind: kindExpr.as("kind"),
370
+ source: sourceExpr.as("source"),
371
+ medium: mediumExpr.as("medium"),
372
+ campaign: campaignExpr.as("campaign"),
269
373
  revenue: sum(purchases.totalAmount),
270
374
  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)));
375
+ }).from(purchases).where(and(...conditions)).groupBy(sql`1`, sql`2`, sql`3`, sql`4`).orderBy(desc(sum(purchases.totalAmount)), desc(count()));
272
376
  return rows.map((r) => ({
377
+ kind: r.kind ?? "unknown",
273
378
  source: r.source ?? null,
274
379
  medium: r.medium ?? null,
275
380
  campaign: r.campaign ?? null,
@@ -296,10 +401,7 @@ function createDatabaseProvider(db, schema) {
296
401
  }).from(purchases).where(and(...purchaseConditions));
297
402
  const [attributedCount] = await db.select({
298
403
  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
- )`));
404
+ }).from(purchases).where(and(...purchaseConditions, attributionSignalSql));
303
405
  const totalSignups = userCount?.total ?? 0;
304
406
  const totalPurchases = purchaseCount?.total ?? 0;
305
407
  const attributedPurchases = attributedCount?.total ?? 0;
@@ -312,33 +414,146 @@ function createDatabaseProvider(db, schema) {
312
414
  };
313
415
  }
314
416
  __name(getConversionFunnel, "getConversionFunnel");
315
- async function getAttributedRevenueSummary(range = "30d") {
417
+ async function getCommerceLaneSummary(range = "30d", filters = {}) {
418
+ const since = rangeToDate(range);
419
+ const conditions = [
420
+ commerceRecord()
421
+ ];
422
+ if (since)
423
+ conditions.push(gte(purchases.createdAt, since));
424
+ if (filters.productId)
425
+ conditions.push(eq(purchases.productId, filters.productId));
426
+ const [commerce] = await db.select({
427
+ count: count()
428
+ }).from(purchases).where(and(...conditions));
429
+ const [paid] = await db.select({
430
+ count: count(),
431
+ revenue: sum(purchases.totalAmount)
432
+ }).from(purchases).where(and(...conditions, paidPurchase()));
433
+ const [grants] = await db.select({
434
+ count: count(),
435
+ revenue: sum(purchases.totalAmount)
436
+ }).from(purchases).where(and(...conditions, accessGrant()));
437
+ const [freeUpgrades] = await db.select({
438
+ count: count()
439
+ }).from(purchases).where(and(...conditions, backfillFreeUpgrade()));
440
+ const [synthetic] = await db.select({
441
+ count: count()
442
+ }).from(purchases).where(and(...conditions, syntheticPurchase()));
443
+ const reasonRows = await db.select({
444
+ reason: sql`COALESCE(
445
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
446
+ 'access_grant'
447
+ )`.as("reason"),
448
+ count: count()
449
+ }).from(purchases).where(and(...conditions, accessGrant())).groupBy(sql`COALESCE(
450
+ NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
451
+ 'access_grant'
452
+ )`);
453
+ return {
454
+ commerceRecords: commerce?.count ?? 0,
455
+ paidPurchases: paid?.count ?? 0,
456
+ paidRevenue: Number(paid?.revenue ?? 0),
457
+ accessGrants: grants?.count ?? 0,
458
+ accessGrantRevenue: Number(grants?.revenue ?? 0),
459
+ freeUpgrades: freeUpgrades?.count ?? 0,
460
+ syntheticPurchases: synthetic?.count ?? 0,
461
+ byAccessGrantReason: Object.fromEntries(reasonRows.map((row) => [
462
+ row.reason,
463
+ row.count
464
+ ]))
465
+ };
466
+ }
467
+ __name(getCommerceLaneSummary, "getCommerceLaneSummary");
468
+ async function getAttributedRevenueSummary(range = "30d", filters = {}) {
316
469
  const since = rangeToDate(range);
317
470
  const conditions = [
318
471
  paidPurchase()
319
472
  ];
320
473
  if (since)
321
474
  conditions.push(gte(purchases.createdAt, since));
475
+ if (filters.productId)
476
+ conditions.push(eq(purchases.productId, filters.productId));
322
477
  const [totals] = await db.select({
323
478
  total: sum(purchases.totalAmount),
324
479
  count: count()
325
480
  }).from(purchases).where(and(...conditions));
326
481
  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
- )`));
482
+ total: sum(purchases.totalAmount),
483
+ count: count()
484
+ }).from(purchases).where(and(...conditions, attributionSignalSql));
485
+ const [purchaseFieldAttributed] = await db.select({
486
+ total: sum(purchases.totalAmount),
487
+ count: count()
488
+ }).from(purchases).where(and(...conditions, purchaseFieldAttributionSignalSql));
489
+ const [recoveredFromShortlinkAttributionTable] = await db.select({
490
+ total: sum(purchases.totalAmount),
491
+ count: count()
492
+ }).from(purchases).where(and(...conditions, shortlinkRecoveredAttributionSignalSql));
493
+ const signalConditions = since ? [
494
+ commerceRecord(),
495
+ gte(purchases.createdAt, since)
496
+ ] : [
497
+ commerceRecord()
498
+ ];
499
+ if (filters.productId) {
500
+ signalConditions.push(eq(purchases.productId, filters.productId));
501
+ }
502
+ const signalCount = /* @__PURE__ */ __name(async (condition) => {
503
+ const [row] = await db.select({
504
+ count: count()
505
+ }).from(purchases).where(and(...signalConditions, condition));
506
+ return row?.count ?? 0;
507
+ }, "signalCount");
508
+ const [shortlinkCount, utmCount, clickIdCount, gaClientIdCount, selfReportedSourceCount, recoveredFromShortlinkAttributionTableCount] = await Promise.all([
509
+ signalCount(sql`(
510
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL
511
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
512
+ )`),
513
+ signalCount(sql`(
514
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
515
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
516
+ )`),
517
+ signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL`),
518
+ signalCount(sql`(
519
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
520
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
521
+ )`),
522
+ signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL`),
523
+ signalCount(shortlinkRecoveredAttributionSignalSql)
524
+ ]);
525
+ const commerce = await getCommerceLaneSummary(range, filters);
332
526
  const totalRevenue = Number(totals?.total ?? 0);
333
527
  const attributedRevenue = Number(attributed?.total ?? 0);
528
+ const purchaseFieldAttributedRevenue = Number(purchaseFieldAttributed?.total ?? 0);
529
+ const recoveredFromShortlinkAttributionTableRevenue = Number(recoveredFromShortlinkAttributionTable?.total ?? 0);
334
530
  const unattributedRevenue = totalRevenue - attributedRevenue;
335
- const totalPurchases = totals?.count ?? 0;
531
+ const paidPurchases = totals?.count ?? 0;
336
532
  return {
337
533
  totalRevenue,
338
534
  attributedRevenue,
339
535
  unattributedRevenue,
340
536
  attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
341
- totalPurchases
537
+ totalPurchases: commerce.commerceRecords,
538
+ paidPurchases,
539
+ purchaseFieldAttributedPurchases: purchaseFieldAttributed?.count ?? 0,
540
+ purchaseFieldAttributedRevenue,
541
+ recoveredFromShortlinkAttributionTablePurchases: recoveredFromShortlinkAttributionTable?.count ?? 0,
542
+ recoveredFromShortlinkAttributionTableRevenue,
543
+ commerceRecords: commerce.commerceRecords,
544
+ accessGrants: commerce.accessGrants,
545
+ freeUpgrades: commerce.freeUpgrades,
546
+ syntheticPurchases: commerce.syntheticPurchases,
547
+ commerce,
548
+ signals: {
549
+ shortlink: shortlinkCount,
550
+ utm: utmCount,
551
+ paidClickId: clickIdCount,
552
+ gaClientId: gaClientIdCount,
553
+ selfReportedSource: selfReportedSourceCount,
554
+ recoveredFromShortlinkAttributionTable: recoveredFromShortlinkAttributionTableCount,
555
+ internalFreeUpgrade: commerce.freeUpgrades
556
+ }
342
557
  };
343
558
  }
344
559
  __name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
@@ -365,6 +580,419 @@ function createDatabaseProvider(db, schema) {
365
580
  }));
366
581
  }
367
582
  __name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
583
+ function parseJsonRecord(value) {
584
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
585
+ }
586
+ __name(parseJsonRecord, "parseJsonRecord");
587
+ function hasRecordValue(value) {
588
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.length > 0);
589
+ }
590
+ __name(hasRecordValue, "hasRecordValue");
591
+ function hasSyntheticClickId(value) {
592
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.startsWith("TEST_"));
593
+ }
594
+ __name(hasSyntheticClickId, "hasSyntheticClickId");
595
+ async function getCheckoutAttributionReceipt(opts) {
596
+ if (!opts.purchaseId) {
597
+ return {
598
+ purchase: null,
599
+ checks: {
600
+ purchaseFound: false,
601
+ attributionSnapshot: false,
602
+ utm: false,
603
+ actualUtm: false,
604
+ clickId: false,
605
+ synthetic: false,
606
+ shortlink: false,
607
+ selfReportedSource: false,
608
+ gaClientId: false
609
+ },
610
+ attribution: null,
611
+ legacy: {},
612
+ fieldKeys: []
613
+ };
614
+ }
615
+ const [row] = await db.select({
616
+ id: purchases.id,
617
+ createdAt: purchases.createdAt,
618
+ productId: purchases.productId,
619
+ productName: products.name,
620
+ totalAmount: purchases.totalAmount,
621
+ status: purchases.status,
622
+ country: purchases.country,
623
+ fields: purchases.fields
624
+ }).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
625
+ if (!row) {
626
+ return {
627
+ purchase: null,
628
+ checks: {
629
+ purchaseFound: false,
630
+ attributionSnapshot: false,
631
+ utm: false,
632
+ actualUtm: false,
633
+ clickId: false,
634
+ synthetic: false,
635
+ shortlink: false,
636
+ selfReportedSource: false,
637
+ gaClientId: false
638
+ },
639
+ attribution: null,
640
+ legacy: {},
641
+ fieldKeys: []
642
+ };
643
+ }
644
+ const fields = parseJsonRecord(row.fields);
645
+ const attribution = parseJsonRecord(fields.attribution);
646
+ const utm = parseJsonRecord(attribution.utm);
647
+ const clickIds = parseJsonRecord(attribution.clickIds);
648
+ const shortlinkSnapshot = parseJsonRecord(attribution.shortlink);
649
+ const ga = parseJsonRecord(attribution.ga);
650
+ const legacy = Object.fromEntries([
651
+ "utmSource",
652
+ "utmMedium",
653
+ "utmCampaign",
654
+ "gaClientId",
655
+ "shortlinkRef",
656
+ "landingPath",
657
+ "referrer",
658
+ "ltUtmSource",
659
+ "ltUtmMedium",
660
+ "ltUtmCampaign"
661
+ ].map((key) => [
662
+ key,
663
+ fields[key]
664
+ ]).filter(([, value]) => value !== void 0 && value !== null));
665
+ const hasActualUtm = Boolean(utm.source || utm.medium || utm.campaign || fields.utmSource);
666
+ const synthetic = Boolean(attribution.synthetic || fields.synthetic || hasSyntheticClickId(clickIds));
667
+ return {
668
+ purchase: {
669
+ id: row.id,
670
+ createdAt: row.createdAt,
671
+ productId: row.productId,
672
+ productName: row.productName ?? null,
673
+ totalAmount: Number(row.totalAmount ?? 0),
674
+ status: row.status,
675
+ country: row.country
676
+ },
677
+ checks: {
678
+ purchaseFound: true,
679
+ attributionSnapshot: Object.keys(attribution).length > 0,
680
+ utm: hasActualUtm,
681
+ actualUtm: hasActualUtm,
682
+ clickId: hasRecordValue(clickIds),
683
+ synthetic,
684
+ shortlink: Boolean(shortlinkSnapshot.slug || fields.shortlinkRef),
685
+ selfReportedSource: Boolean(attribution.selfReportedSource),
686
+ gaClientId: Boolean(ga.clientId || fields.gaClientId)
687
+ },
688
+ attribution: Object.keys(attribution).length > 0 ? attribution : null,
689
+ legacy,
690
+ fieldKeys: Object.keys(fields).sort()
691
+ };
692
+ }
693
+ __name(getCheckoutAttributionReceipt, "getCheckoutAttributionReceipt");
694
+ async function getCheckoutSurveyFallbackReport(range = "30d", limit = 20, filters = {}) {
695
+ const label = "recovered_from_checkout_survey_response";
696
+ if (!questionResponse) {
697
+ return {
698
+ label,
699
+ productId: filters.productId ?? null,
700
+ totalDarkPurchases: 0,
701
+ totalDarkRevenue: 0,
702
+ exactPurchaseLinkedPurchases: 0,
703
+ exactPurchaseLinkedRevenue: 0,
704
+ userLinkedPurchases: 0,
705
+ userLinkedRevenue: 0,
706
+ byAnswer: [],
707
+ notes: [
708
+ "questionResponse table was not provided to this provider"
709
+ ]
710
+ };
711
+ }
712
+ const since = rangeToDate(range);
713
+ const purchaseConditions = [
714
+ paidPurchase(),
715
+ sql`NOT ${attributionSignalSql}`
716
+ ];
717
+ if (since)
718
+ purchaseConditions.push(gte(purchases.createdAt, since));
719
+ if (filters.productId) {
720
+ purchaseConditions.push(eq(purchases.productId, filters.productId));
721
+ }
722
+ const darkRows = await db.select({
723
+ id: purchases.id,
724
+ userId: purchases.userId,
725
+ revenue: purchases.totalAmount
726
+ }).from(purchases).where(and(...purchaseConditions));
727
+ const totalDarkRevenue = darkRows.reduce((total, row) => total + Number(row.revenue ?? 0), 0);
728
+ if (darkRows.length === 0) {
729
+ return {
730
+ label,
731
+ productId: filters.productId ?? null,
732
+ totalDarkPurchases: 0,
733
+ totalDarkRevenue: 0,
734
+ exactPurchaseLinkedPurchases: 0,
735
+ exactPurchaseLinkedRevenue: 0,
736
+ userLinkedPurchases: 0,
737
+ userLinkedRevenue: 0,
738
+ byAnswer: [],
739
+ notes: [
740
+ "No dark paid purchases matched the filters"
741
+ ]
742
+ };
743
+ }
744
+ const darkById = new Map(darkRows.map((row) => [
745
+ row.id,
746
+ row
747
+ ]));
748
+ const darkByUserId = /* @__PURE__ */ new Map();
749
+ for (const row of darkRows) {
750
+ if (!row.userId)
751
+ continue;
752
+ const bucket = darkByUserId.get(row.userId) ?? [];
753
+ bucket.push(row);
754
+ darkByUserId.set(row.userId, bucket);
755
+ }
756
+ const responseConditions = [];
757
+ if (since)
758
+ responseConditions.push(gte(questionResponse.createdAt, since));
759
+ const darkIds = [
760
+ ...darkById.keys()
761
+ ];
762
+ const darkUserIds = [
763
+ ...darkByUserId.keys()
764
+ ];
765
+ const exactPurchaseCondition = sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId')) IN (${sql.join(darkIds.map((id) => sql`${id}`), sql`, `)})`;
766
+ const userCondition = darkUserIds.length > 0 ? sql`${questionResponse.userId} IN (${sql.join(darkUserIds.map((id) => sql`${id}`), sql`, `)})` : sql`FALSE`;
767
+ responseConditions.push(sql`(${exactPurchaseCondition} OR ${userCondition})`);
768
+ const responseRows = await db.select({
769
+ purchaseId: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId'))`,
770
+ answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
771
+ userId: questionResponse.userId,
772
+ createdAt: questionResponse.createdAt
773
+ }).from(questionResponse).where(and(...responseConditions));
774
+ const exactByPurchase = /* @__PURE__ */ new Map();
775
+ const latestByUser = /* @__PURE__ */ new Map();
776
+ for (const row of responseRows) {
777
+ if (row.purchaseId && darkById.has(row.purchaseId)) {
778
+ exactByPurchase.set(row.purchaseId, row);
779
+ continue;
780
+ }
781
+ if (!row.userId)
782
+ continue;
783
+ const current = latestByUser.get(row.userId);
784
+ const rowTime = new Date(row.createdAt ?? 0).getTime();
785
+ const currentTime = current ? new Date(current.createdAt ?? 0).getTime() : -1;
786
+ if (!current || rowTime >= currentTime)
787
+ latestByUser.set(row.userId, row);
788
+ }
789
+ const classified = /* @__PURE__ */ new Map();
790
+ for (const [purchaseId, response] of exactByPurchase) {
791
+ const purchase = darkById.get(purchaseId);
792
+ classified.set(purchaseId, {
793
+ confidence: "exact_purchase_link",
794
+ answer: response.answer || "(no answer)",
795
+ revenue: Number(purchase?.revenue ?? 0)
796
+ });
797
+ }
798
+ for (const row of darkRows) {
799
+ if (classified.has(row.id) || !row.userId)
800
+ continue;
801
+ const response = latestByUser.get(row.userId);
802
+ if (!response)
803
+ continue;
804
+ classified.set(row.id, {
805
+ confidence: "user_linked",
806
+ answer: response.answer || "(no answer)",
807
+ revenue: Number(row.revenue ?? 0)
808
+ });
809
+ }
810
+ const byAnswer = /* @__PURE__ */ new Map();
811
+ let exactPurchaseLinkedPurchases = 0;
812
+ let exactPurchaseLinkedRevenue = 0;
813
+ let userLinkedPurchases = 0;
814
+ let userLinkedRevenue = 0;
815
+ for (const entry of classified.values()) {
816
+ if (entry.confidence === "exact_purchase_link") {
817
+ exactPurchaseLinkedPurchases += 1;
818
+ exactPurchaseLinkedRevenue += entry.revenue;
819
+ } else {
820
+ userLinkedPurchases += 1;
821
+ userLinkedRevenue += entry.revenue;
822
+ }
823
+ const key = `${entry.confidence}:${entry.answer}`;
824
+ const current = byAnswer.get(key) ?? {
825
+ answer: entry.answer,
826
+ confidence: entry.confidence,
827
+ purchases: 0,
828
+ revenue: 0
829
+ };
830
+ current.purchases += 1;
831
+ current.revenue += entry.revenue;
832
+ byAnswer.set(key, current);
833
+ }
834
+ return {
835
+ label,
836
+ productId: filters.productId ?? null,
837
+ totalDarkPurchases: darkRows.length,
838
+ totalDarkRevenue,
839
+ exactPurchaseLinkedPurchases,
840
+ exactPurchaseLinkedRevenue,
841
+ userLinkedPurchases,
842
+ userLinkedRevenue,
843
+ byAnswer: [
844
+ ...byAnswer.values()
845
+ ].sort((a, b) => b.revenue - a.revenue || b.purchases - a.purchases).slice(0, limit),
846
+ notes: [
847
+ "Report-only fallback. No purchase fields are mutated.",
848
+ "Exact purchase-linked responses use QuestionResponse.fields.purchaseId when present.",
849
+ "User-linked responses are lower-confidence and remain report-only."
850
+ ]
851
+ };
852
+ }
853
+ __name(getCheckoutSurveyFallbackReport, "getCheckoutSurveyFallbackReport");
854
+ async function getValuePathSummary(range = "30d") {
855
+ if (!contactEvent || !sideEffectIntent) {
856
+ return {
857
+ contacts: 0,
858
+ events: 0,
859
+ intents: 0,
860
+ completedIntents: 0,
861
+ pendingIntents: 0,
862
+ blockedIntents: 0,
863
+ answerEvents: 0,
864
+ dripEvents: 0,
865
+ enteredEvents: 0,
866
+ participantsWithAnswerClicks: 0,
867
+ participantsWithNoAnswerClicks: 0,
868
+ terminalParticipants: 0,
869
+ answerOptions: [],
870
+ answerSteps: [],
871
+ terminalSteps: [],
872
+ notes: [
873
+ "Contact Event and SideEffectIntent tables are unavailable."
874
+ ]
875
+ };
876
+ }
877
+ const since = rangeToDate(range);
878
+ const eventConditions = [
879
+ sql`${contactEvent.eventType} LIKE 'value-path.%'`
880
+ ];
881
+ const intentConditions = [
882
+ eq(sideEffectIntent.type, "send-value-path-email")
883
+ ];
884
+ if (since) {
885
+ eventConditions.push(gte(contactEvent.occurredAt, since));
886
+ intentConditions.push(gte(sideEffectIntent.createdAt, since));
887
+ }
888
+ const [events, intents] = await Promise.all([
889
+ db.select({
890
+ contactId: contactEvent.contactId,
891
+ eventType: contactEvent.eventType,
892
+ providerEventId: contactEvent.providerEventId,
893
+ occurredAt: contactEvent.occurredAt
894
+ }).from(contactEvent).where(and(...eventConditions)),
895
+ db.select({
896
+ contactId: sideEffectIntent.contactId,
897
+ status: sideEffectIntent.status,
898
+ metadata: sideEffectIntent.metadata,
899
+ createdAt: sideEffectIntent.createdAt
900
+ }).from(sideEffectIntent).where(and(...intentConditions))
901
+ ]);
902
+ const contactIds = /* @__PURE__ */ new Set();
903
+ for (const event of events)
904
+ contactIds.add(event.contactId);
905
+ for (const intent of intents)
906
+ contactIds.add(intent.contactId);
907
+ const answerEvents = events.filter((event) => event.eventType === "value-path.answer-selected");
908
+ const dripEvents = events.filter((event) => event.eventType === "value-path.drip-progressed");
909
+ const enteredEvents = events.filter((event) => event.eventType === "value-path.entered");
910
+ const answersByContact = /* @__PURE__ */ new Map();
911
+ for (const event of answerEvents) {
912
+ answersByContact.set(event.contactId, (answersByContact.get(event.contactId) ?? 0) + 1);
913
+ }
914
+ const answerKeyCounts = /* @__PURE__ */ new Map();
915
+ const answerStepCounts = /* @__PURE__ */ new Map();
916
+ for (const event of answerEvents) {
917
+ const key = parseValuePathAnswerKey(event.providerEventId);
918
+ answerKeyCounts.set(key, (answerKeyCounts.get(key) ?? 0) + 1);
919
+ const step = answerStepFromKey(key);
920
+ answerStepCounts.set(step, (answerStepCounts.get(step) ?? 0) + 1);
921
+ }
922
+ const latestIntentByContact = /* @__PURE__ */ new Map();
923
+ for (const intent of [
924
+ ...intents
925
+ ].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())) {
926
+ latestIntentByContact.set(intent.contactId, intent);
927
+ }
928
+ const terminalCounts = /* @__PURE__ */ new Map();
929
+ for (const intent of latestIntentByContact.values()) {
930
+ const emailResourceId = String(intent.metadata?.emailResourceId ?? "");
931
+ if (!isTerminalValuePathEmail(emailResourceId))
932
+ continue;
933
+ terminalCounts.set(emailResourceId, (terminalCounts.get(emailResourceId) ?? 0) + 1);
934
+ }
935
+ return {
936
+ contacts: contactIds.size,
937
+ events: events.length,
938
+ intents: intents.length,
939
+ completedIntents: intents.filter((intent) => intent.status === "completed").length,
940
+ pendingIntents: intents.filter((intent) => intent.status === "pending").length,
941
+ blockedIntents: intents.filter((intent) => intent.status === "blocked").length,
942
+ answerEvents: answerEvents.length,
943
+ dripEvents: dripEvents.length,
944
+ enteredEvents: enteredEvents.length,
945
+ participantsWithAnswerClicks: answersByContact.size,
946
+ participantsWithNoAnswerClicks: Math.max(0, contactIds.size - answersByContact.size),
947
+ terminalParticipants: [
948
+ ...terminalCounts.values()
949
+ ].reduce((total, count2) => total + count2, 0),
950
+ answerOptions: [
951
+ ...answerKeyCounts.entries()
952
+ ].map(([key, count2]) => ({
953
+ key,
954
+ step: answerStepFromKey(key),
955
+ optionValue: answerOptionFromKey(key),
956
+ count: count2
957
+ })).sort((a, b) => b.count - a.count),
958
+ answerSteps: [
959
+ ...answerStepCounts.entries()
960
+ ].map(([step, count2]) => ({
961
+ step,
962
+ count: count2
963
+ })).sort((a, b) => b.count - a.count),
964
+ terminalSteps: [
965
+ ...terminalCounts.entries()
966
+ ].map(([emailResourceId, count2]) => ({
967
+ emailResourceId,
968
+ count: count2
969
+ })).sort((a, b) => b.count - a.count),
970
+ notes: [
971
+ "Counts are based on Contact Events and SideEffectIntents.",
972
+ "Kit-native opens, unsubscribes, complaints, and raw Kit link clicks are not included."
973
+ ]
974
+ };
975
+ }
976
+ __name(getValuePathSummary, "getValuePathSummary");
977
+ function parseValuePathAnswerKey(providerEventId) {
978
+ const match = String(providerEventId ?? "").match(/:answer:(.+)$/);
979
+ return match?.[1] ?? "unknown";
980
+ }
981
+ __name(parseValuePathAnswerKey, "parseValuePathAnswerKey");
982
+ function answerStepFromKey(key) {
983
+ const index = key.lastIndexOf(".");
984
+ return index > -1 ? key.slice(0, index) : key;
985
+ }
986
+ __name(answerStepFromKey, "answerStepFromKey");
987
+ function answerOptionFromKey(key) {
988
+ const index = key.lastIndexOf(".");
989
+ return index > -1 ? key.slice(index + 1) : "unknown";
990
+ }
991
+ __name(answerOptionFromKey, "answerOptionFromKey");
992
+ function isTerminalValuePathEmail(emailResourceId) {
993
+ return /(?:^|\.)(?:email-6|team-email-6)$/.test(emailResourceId);
994
+ }
995
+ __name(isTerminalValuePathEmail, "isTerminalValuePathEmail");
368
996
  async function traceAttribution(opts) {
369
997
  const events = [];
370
998
  let userId = null;
@@ -521,8 +1149,12 @@ function createDatabaseProvider(db, schema) {
521
1149
  getShortlinkPerformance,
522
1150
  getRevenueBySource,
523
1151
  getConversionFunnel,
1152
+ getCommerceLaneSummary,
524
1153
  getAttributedRevenueSummary,
525
1154
  getContentPurchaseCorrelation,
1155
+ getCheckoutAttributionReceipt,
1156
+ getCheckoutSurveyFallbackReport,
1157
+ getValuePathSummary,
526
1158
  traceAttribution
527
1159
  };
528
1160
  }