@coursebuilder/analytics 1.1.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api/index.d.ts +158 -0
  3. package/dist/api/index.js +317 -0
  4. package/dist/api/index.js.map +1 -0
  5. package/dist/catalog.d.ts +14 -0
  6. package/dist/catalog.js +209 -0
  7. package/dist/catalog.js.map +1 -0
  8. package/dist/components/index.d.ts +172 -0
  9. package/dist/components/index.js +1258 -0
  10. package/dist/components/index.js.map +1 -0
  11. package/dist/engine.d.ts +20 -0
  12. package/dist/engine.js +350 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +353 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/providers/database.d.ts +79 -0
  18. package/dist/providers/database.js +533 -0
  19. package/dist/providers/database.js.map +1 -0
  20. package/dist/providers/derived.d.ts +45 -0
  21. package/dist/providers/derived.js +32 -0
  22. package/dist/providers/derived.js.map +1 -0
  23. package/dist/providers/ga4.d.ts +43 -0
  24. package/dist/providers/ga4.js +220 -0
  25. package/dist/providers/ga4.js.map +1 -0
  26. package/dist/providers/index.d.ts +8 -0
  27. package/dist/providers/index.js +1239 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/mux.d.ts +103 -0
  30. package/dist/providers/mux.js +241 -0
  31. package/dist/providers/mux.js.map +1 -0
  32. package/dist/providers/survey.d.ts +102 -0
  33. package/dist/providers/survey.js +233 -0
  34. package/dist/providers/survey.js.map +1 -0
  35. package/dist/types.d.ts +303 -0
  36. package/dist/types.js +1 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +101 -0
  39. package/src/api/catalog-handler.ts +321 -0
  40. package/src/api/index.ts +4 -0
  41. package/src/api/token-handler.ts +71 -0
  42. package/src/catalog.ts +223 -0
  43. package/src/components/country-chart.tsx +114 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/omnibus-dashboard.tsx +1460 -0
  46. package/src/components/revenue-chart.tsx +251 -0
  47. package/src/components/use-chart-colors.ts +75 -0
  48. package/src/engine.ts +201 -0
  49. package/src/index.ts +7 -0
  50. package/src/providers/database.ts +795 -0
  51. package/src/providers/derived.ts +79 -0
  52. package/src/providers/ga4.ts +173 -0
  53. package/src/providers/index.ts +44 -0
  54. package/src/providers/mux.ts +438 -0
  55. package/src/providers/survey.ts +487 -0
  56. package/src/types.ts +333 -0
@@ -0,0 +1,533 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/providers/database.ts
5
+ import { and, count, desc, eq, gt, gte, inArray, lte, sql, sum } from "drizzle-orm";
6
+ function createDatabaseProvider(db, schema) {
7
+ const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick } = schema;
8
+ const PAID_STATUSES = [
9
+ "Valid",
10
+ "Restricted"
11
+ ];
12
+ function paidPurchase() {
13
+ return inArray(purchases.status, [
14
+ ...PAID_STATUSES
15
+ ]);
16
+ }
17
+ __name(paidPurchase, "paidPurchase");
18
+ function rangeToDate(range) {
19
+ if (range === "all")
20
+ return null;
21
+ const now = /* @__PURE__ */ new Date();
22
+ const hours = {
23
+ "24h": 24,
24
+ "7d": 7 * 24,
25
+ "30d": 30 * 24,
26
+ "90d": 90 * 24
27
+ };
28
+ return new Date(now.getTime() - (hours[range] ?? 30 * 24) * 60 * 60 * 1e3);
29
+ }
30
+ __name(rangeToDate, "rangeToDate");
31
+ async function getRevenueSummary(range = "30d") {
32
+ const since = rangeToDate(range);
33
+ const conditions = [
34
+ paidPurchase()
35
+ ];
36
+ if (since)
37
+ conditions.push(gte(purchases.createdAt, since));
38
+ const [totals] = await db.select({
39
+ totalRevenue: sum(purchases.totalAmount),
40
+ purchaseCount: count()
41
+ }).from(purchases).where(and(...conditions));
42
+ return {
43
+ totalRevenue: Number(totals?.totalRevenue ?? 0),
44
+ purchaseCount: totals?.purchaseCount ?? 0,
45
+ avgOrderValue: totals?.purchaseCount && totals.purchaseCount > 0 ? Number(totals.totalRevenue ?? 0) / totals.purchaseCount : 0
46
+ };
47
+ }
48
+ __name(getRevenueSummary, "getRevenueSummary");
49
+ async function getRevenueByDay(range = "30d") {
50
+ const since = rangeToDate(range);
51
+ const conditions = [
52
+ paidPurchase()
53
+ ];
54
+ if (since)
55
+ conditions.push(gte(purchases.createdAt, since));
56
+ const rows = await db.select({
57
+ date: sql`DATE(${purchases.createdAt})`.as("date"),
58
+ revenue: sum(purchases.totalAmount),
59
+ count: count()
60
+ }).from(purchases).where(and(...conditions)).groupBy(sql`DATE(${purchases.createdAt})`).orderBy(sql`DATE(${purchases.createdAt})`);
61
+ return rows.map((r) => ({
62
+ date: r.date,
63
+ revenue: Number(r.revenue ?? 0),
64
+ count: r.count
65
+ }));
66
+ }
67
+ __name(getRevenueByDay, "getRevenueByDay");
68
+ async function getPreviousPeriodRevenueByDay(range = "30d") {
69
+ if (range === "all")
70
+ return [];
71
+ const hours = {
72
+ "24h": 24,
73
+ "7d": 7 * 24,
74
+ "30d": 30 * 24,
75
+ "90d": 90 * 24
76
+ };
77
+ const periodMs = (hours[range] ?? 30 * 24) * 60 * 60 * 1e3;
78
+ const now = /* @__PURE__ */ new Date();
79
+ const periodStart = new Date(now.getTime() - periodMs);
80
+ const prevStart = new Date(periodStart.getTime() - periodMs);
81
+ const rows = await db.select({
82
+ date: sql`DATE(${purchases.createdAt})`.as("date"),
83
+ revenue: sum(purchases.totalAmount),
84
+ count: count()
85
+ }).from(purchases).where(and(paidPurchase(), gte(purchases.createdAt, prevStart), lte(purchases.createdAt, periodStart))).groupBy(sql`DATE(${purchases.createdAt})`).orderBy(sql`DATE(${purchases.createdAt})`);
86
+ return rows.map((r) => ({
87
+ date: r.date,
88
+ revenue: Number(r.revenue ?? 0),
89
+ count: r.count
90
+ }));
91
+ }
92
+ __name(getPreviousPeriodRevenueByDay, "getPreviousPeriodRevenueByDay");
93
+ async function getRevenueByProduct(range = "30d") {
94
+ const since = rangeToDate(range);
95
+ const conditions = [
96
+ paidPurchase()
97
+ ];
98
+ if (since)
99
+ conditions.push(gte(purchases.createdAt, since));
100
+ const rows = await db.select({
101
+ productId: purchases.productId,
102
+ productName: products.name,
103
+ revenue: sum(purchases.totalAmount),
104
+ count: count()
105
+ }).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(and(...conditions)).groupBy(purchases.productId, products.name).orderBy(desc(sum(purchases.totalAmount)));
106
+ return rows.map((r) => ({
107
+ productId: r.productId,
108
+ productName: r.productName ?? "(unknown)",
109
+ revenue: Number(r.revenue ?? 0),
110
+ count: r.count
111
+ }));
112
+ }
113
+ __name(getRevenueByProduct, "getRevenueByProduct");
114
+ async function getRevenueByCountry(range = "30d") {
115
+ const since = rangeToDate(range);
116
+ const conditions = [
117
+ paidPurchase()
118
+ ];
119
+ if (since)
120
+ conditions.push(gte(purchases.createdAt, since));
121
+ const rows = await db.select({
122
+ country: purchases.country,
123
+ revenue: sum(purchases.totalAmount),
124
+ count: count()
125
+ }).from(purchases).where(and(...conditions)).groupBy(purchases.country).orderBy(desc(sum(purchases.totalAmount))).limit(20);
126
+ return rows.map((r) => ({
127
+ country: r.country ?? "(unknown)",
128
+ revenue: Number(r.revenue ?? 0),
129
+ count: r.count
130
+ }));
131
+ }
132
+ __name(getRevenueByCountry, "getRevenueByCountry");
133
+ async function getRecentPurchases(limit = 20, filter = "all", range = "all") {
134
+ const since = rangeToDate(range);
135
+ const conditions = [
136
+ paidPurchase()
137
+ ];
138
+ if (since)
139
+ conditions.push(gte(purchases.createdAt, since));
140
+ if (filter === "team") {
141
+ conditions.push(sql`${purchases.bulkCouponId} IS NOT NULL`);
142
+ const rows2 = await db.select({
143
+ id: purchases.id,
144
+ createdAt: purchases.createdAt,
145
+ totalAmount: purchases.totalAmount,
146
+ productName: products.name,
147
+ productId: purchases.productId,
148
+ country: purchases.country,
149
+ couponId: purchases.couponId,
150
+ userId: purchases.userId,
151
+ userName: users.name,
152
+ userEmail: users.email,
153
+ organizationId: purchases.organizationId,
154
+ seats: coupon.maxUses
155
+ }).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).leftJoin(users, eq(purchases.userId, users.id)).leftJoin(coupon, eq(purchases.bulkCouponId, coupon.id)).where(and(...conditions, gt(coupon.maxUses, 1))).orderBy(desc(purchases.totalAmount)).limit(limit);
156
+ return rows2.map((r) => ({
157
+ id: r.id,
158
+ createdAt: r.createdAt,
159
+ totalAmount: Number(r.totalAmount),
160
+ productName: r.productName ?? "(unknown)",
161
+ productId: r.productId,
162
+ country: r.country,
163
+ couponId: r.couponId,
164
+ userName: r.userName ?? null,
165
+ userEmail: r.userEmail ?? null,
166
+ isTeam: true,
167
+ seats: r.seats ?? null
168
+ }));
169
+ }
170
+ if (filter === "individual") {
171
+ conditions.push(sql`${purchases.bulkCouponId} IS NULL`);
172
+ }
173
+ const rows = await db.query.purchases.findMany({
174
+ where: and(...conditions),
175
+ orderBy: [
176
+ desc(purchases.totalAmount)
177
+ ],
178
+ limit,
179
+ with: {
180
+ product: true,
181
+ user: true
182
+ }
183
+ });
184
+ return rows.map((r) => ({
185
+ id: r.id,
186
+ createdAt: r.createdAt,
187
+ totalAmount: Number(r.totalAmount),
188
+ productName: r.product?.name ?? "(unknown)",
189
+ productId: r.productId,
190
+ country: r.country,
191
+ couponId: r.couponId,
192
+ userName: r.user?.name ?? null,
193
+ userEmail: r.user?.email ?? null,
194
+ isTeam: r.organizationId != null,
195
+ seats: null
196
+ }));
197
+ }
198
+ __name(getRecentPurchases, "getRecentPurchases");
199
+ async function getAttributionSummary(range = "30d") {
200
+ const since = rangeToDate(range);
201
+ const conditions = [];
202
+ if (since)
203
+ conditions.push(gte(shortlinkAttribution.createdAt, since));
204
+ const rows = await db.select({
205
+ type: shortlinkAttribution.type,
206
+ count: count()
207
+ }).from(shortlinkAttribution).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(shortlinkAttribution.type);
208
+ return rows.map((r) => ({
209
+ type: r.type,
210
+ count: r.count
211
+ }));
212
+ }
213
+ __name(getAttributionSummary, "getAttributionSummary");
214
+ async function getShortlinkPerformance(range = "30d") {
215
+ const since = rangeToDate(range);
216
+ const clickConditions = [];
217
+ if (since)
218
+ clickConditions.push(gte(shortlinkClick.timestamp, since));
219
+ const rows = await db.select({
220
+ shortlinkId: shortlinkClick.shortlinkId,
221
+ slug: shortlink.slug,
222
+ url: shortlink.url,
223
+ clicks: count()
224
+ }).from(shortlinkClick).innerJoin(shortlink, eq(shortlinkClick.shortlinkId, shortlink.id)).where(clickConditions.length > 0 ? and(...clickConditions) : void 0).groupBy(shortlinkClick.shortlinkId, shortlink.slug, shortlink.url).orderBy(desc(count())).limit(20);
225
+ const attrConditions = [];
226
+ if (since)
227
+ attrConditions.push(gte(shortlinkAttribution.createdAt, since));
228
+ const attrRows = await db.select({
229
+ shortlinkId: shortlinkAttribution.shortlinkId,
230
+ type: shortlinkAttribution.type,
231
+ count: count()
232
+ }).from(shortlinkAttribution).where(attrConditions.length > 0 ? and(...attrConditions) : void 0).groupBy(shortlinkAttribution.shortlinkId, shortlinkAttribution.type);
233
+ const attrMap = /* @__PURE__ */ new Map();
234
+ for (const a of attrRows) {
235
+ const existing = attrMap.get(a.shortlinkId) ?? {
236
+ signups: 0,
237
+ purchases: 0
238
+ };
239
+ if (a.type === "signup")
240
+ existing.signups = a.count;
241
+ if (a.type === "purchase")
242
+ existing.purchases = a.count;
243
+ attrMap.set(a.shortlinkId, existing);
244
+ }
245
+ return rows.map((r) => {
246
+ const attr = attrMap.get(r.shortlinkId);
247
+ return {
248
+ shortlinkId: r.shortlinkId,
249
+ slug: r.slug,
250
+ url: r.url,
251
+ clicks: r.clicks,
252
+ signups: attr?.signups ?? 0,
253
+ purchases: attr?.purchases ?? 0
254
+ };
255
+ });
256
+ }
257
+ __name(getShortlinkPerformance, "getShortlinkPerformance");
258
+ async function getRevenueBySource(range = "30d") {
259
+ const since = rangeToDate(range);
260
+ const conditions = [
261
+ paidPurchase()
262
+ ];
263
+ if (since)
264
+ conditions.push(gte(purchases.createdAt, since));
265
+ 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"),
269
+ revenue: sum(purchases.totalAmount),
270
+ 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)));
272
+ return rows.map((r) => ({
273
+ source: r.source ?? null,
274
+ medium: r.medium ?? null,
275
+ campaign: r.campaign ?? null,
276
+ revenue: Number(r.revenue ?? 0),
277
+ count: r.count
278
+ }));
279
+ }
280
+ __name(getRevenueBySource, "getRevenueBySource");
281
+ async function getConversionFunnel(range = "30d") {
282
+ const since = rangeToDate(range);
283
+ const userConditions = since ? [
284
+ gte(users.createdAt, since)
285
+ ] : [];
286
+ const purchaseConditions = [
287
+ paidPurchase()
288
+ ];
289
+ if (since)
290
+ purchaseConditions.push(gte(purchases.createdAt, since));
291
+ const [userCount] = await db.select({
292
+ total: count()
293
+ }).from(users).where(userConditions.length > 0 ? and(...userConditions) : void 0);
294
+ const [purchaseCount] = await db.select({
295
+ total: count()
296
+ }).from(purchases).where(and(...purchaseConditions));
297
+ const [attributedCount] = await db.select({
298
+ 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
+ )`));
303
+ const totalSignups = userCount?.total ?? 0;
304
+ const totalPurchases = purchaseCount?.total ?? 0;
305
+ const attributedPurchases = attributedCount?.total ?? 0;
306
+ return {
307
+ totalSignups,
308
+ totalPurchases,
309
+ attributedPurchases,
310
+ conversionRate: totalSignups > 0 ? totalPurchases / totalSignups : 0,
311
+ attributionCoverage: totalPurchases > 0 ? attributedPurchases / totalPurchases : 0
312
+ };
313
+ }
314
+ __name(getConversionFunnel, "getConversionFunnel");
315
+ async function getAttributedRevenueSummary(range = "30d") {
316
+ const since = rangeToDate(range);
317
+ const conditions = [
318
+ paidPurchase()
319
+ ];
320
+ if (since)
321
+ conditions.push(gte(purchases.createdAt, since));
322
+ const [totals] = await db.select({
323
+ total: sum(purchases.totalAmount),
324
+ count: count()
325
+ }).from(purchases).where(and(...conditions));
326
+ 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
+ )`));
332
+ const totalRevenue = Number(totals?.total ?? 0);
333
+ const attributedRevenue = Number(attributed?.total ?? 0);
334
+ const unattributedRevenue = totalRevenue - attributedRevenue;
335
+ const totalPurchases = totals?.count ?? 0;
336
+ return {
337
+ totalRevenue,
338
+ attributedRevenue,
339
+ unattributedRevenue,
340
+ attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
341
+ totalPurchases
342
+ };
343
+ }
344
+ __name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
345
+ async function getContentPurchaseCorrelation(range = "30d", limit = 20) {
346
+ const since = rangeToDate(range);
347
+ const purchaseConditions = [
348
+ paidPurchase()
349
+ ];
350
+ if (since)
351
+ purchaseConditions.push(gte(purchases.createdAt, since));
352
+ const purchaserRows = await db.selectDistinct({
353
+ userId: purchases.userId
354
+ }).from(purchases).where(and(...purchaseConditions));
355
+ const purchaserIds = purchaserRows.map((r) => r.userId).filter((id) => id !== null);
356
+ if (purchaserIds.length === 0)
357
+ return [];
358
+ const rows = await db.select({
359
+ resourceId: resourceProgress.resourceId,
360
+ purchaserCount: count()
361
+ }).from(resourceProgress).where(sql`${resourceProgress.userId} IN (${sql.join(purchaserIds.map((id) => sql`${id}`), sql`, `)})`).groupBy(resourceProgress.resourceId).orderBy(desc(count())).limit(limit);
362
+ return rows.map((r) => ({
363
+ resourceId: r.resourceId,
364
+ purchaserCount: r.purchaserCount
365
+ }));
366
+ }
367
+ __name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
368
+ async function traceAttribution(opts) {
369
+ const events = [];
370
+ let userId = null;
371
+ let userEmail = opts.email ?? null;
372
+ let userRecord = null;
373
+ if (opts.purchaseId) {
374
+ const [purchase] = await db.select({
375
+ userId: purchases.userId,
376
+ email: users.email
377
+ }).from(purchases).leftJoin(users, eq(purchases.userId, users.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
378
+ userId = purchase?.userId ?? null;
379
+ userEmail = purchase?.email ?? userEmail;
380
+ }
381
+ if (userEmail && !userId) {
382
+ const [u] = await db.select({
383
+ id: users.id
384
+ }).from(users).where(eq(users.email, userEmail)).limit(1);
385
+ userId = u?.id ?? null;
386
+ }
387
+ if (userId) {
388
+ const [u] = await db.select({
389
+ id: users.id,
390
+ email: users.email,
391
+ name: users.name,
392
+ createdAt: users.createdAt
393
+ }).from(users).where(eq(users.id, userId)).limit(1);
394
+ userRecord = u ? {
395
+ id: u.id,
396
+ email: u.email,
397
+ name: u.name ?? null,
398
+ createdAt: u.createdAt
399
+ } : null;
400
+ }
401
+ const attrConditions = [];
402
+ if (userId)
403
+ attrConditions.push(eq(shortlinkAttribution.userId, userId));
404
+ if (userEmail)
405
+ attrConditions.push(eq(shortlinkAttribution.email, userEmail));
406
+ if (attrConditions.length > 0) {
407
+ const attrs = await db.select({
408
+ type: shortlinkAttribution.type,
409
+ createdAt: shortlinkAttribution.createdAt,
410
+ metadata: shortlinkAttribution.metadata,
411
+ shortlinkId: shortlinkAttribution.shortlinkId,
412
+ slug: shortlink.slug,
413
+ url: shortlink.url
414
+ }).from(shortlinkAttribution).leftJoin(shortlink, eq(shortlinkAttribution.shortlinkId, shortlink.id)).where(sql`(${sql.join(attrConditions, sql` OR `)})`).orderBy(shortlinkAttribution.createdAt);
415
+ for (const attr of attrs) {
416
+ const clicks = await db.select({
417
+ timestamp: shortlinkClick.timestamp,
418
+ referrer: shortlinkClick.referrer,
419
+ country: shortlinkClick.country,
420
+ device: shortlinkClick.device
421
+ }).from(shortlinkClick).where(and(eq(shortlinkClick.shortlinkId, attr.shortlinkId), lte(shortlinkClick.timestamp, attr.createdAt))).orderBy(desc(shortlinkClick.timestamp)).limit(3);
422
+ for (const click of clicks) {
423
+ events.push({
424
+ type: "click",
425
+ timestamp: click.timestamp,
426
+ detail: {
427
+ shortlink: `/s/${attr.slug}`,
428
+ destination: attr.url,
429
+ referrer: click.referrer,
430
+ country: click.country,
431
+ device: click.device
432
+ }
433
+ });
434
+ }
435
+ events.push({
436
+ type: attr.type === "purchase" ? "purchase" : "signup",
437
+ timestamp: attr.createdAt,
438
+ detail: {
439
+ shortlink: `/s/${attr.slug}`,
440
+ metadata: attr.metadata ? JSON.parse(attr.metadata) : null
441
+ }
442
+ });
443
+ }
444
+ }
445
+ if (userId) {
446
+ const progress = await db.select({
447
+ resourceId: resourceProgress.resourceId,
448
+ completedAt: resourceProgress.completedAt,
449
+ createdAt: resourceProgress.createdAt
450
+ }).from(resourceProgress).where(eq(resourceProgress.userId, userId)).orderBy(resourceProgress.createdAt).limit(20);
451
+ for (const p of progress) {
452
+ events.push({
453
+ type: "progress",
454
+ timestamp: p.completedAt ?? p.createdAt,
455
+ detail: {
456
+ resourceId: p.resourceId
457
+ }
458
+ });
459
+ }
460
+ }
461
+ const purchaseConditions = [
462
+ paidPurchase()
463
+ ];
464
+ if (opts.purchaseId) {
465
+ purchaseConditions.push(eq(purchases.id, opts.purchaseId));
466
+ } else if (userId) {
467
+ purchaseConditions.push(eq(purchases.userId, userId));
468
+ } else {
469
+ return {
470
+ user: userRecord,
471
+ events: [],
472
+ purchases: []
473
+ };
474
+ }
475
+ const purchaseRows = await db.select({
476
+ id: purchases.id,
477
+ totalAmount: purchases.totalAmount,
478
+ productName: products.name,
479
+ createdAt: purchases.createdAt,
480
+ country: purchases.country,
481
+ fields: purchases.fields
482
+ }).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(and(...purchaseConditions)).orderBy(purchases.createdAt);
483
+ const purchaseResults = purchaseRows.map((p) => {
484
+ const fields = p.fields ?? {};
485
+ events.push({
486
+ type: "purchase",
487
+ timestamp: p.createdAt,
488
+ detail: {
489
+ purchaseId: p.id,
490
+ amount: Number(p.totalAmount),
491
+ product: p.productName
492
+ }
493
+ });
494
+ return {
495
+ id: p.id,
496
+ totalAmount: Number(p.totalAmount),
497
+ productName: p.productName ?? "Unknown",
498
+ createdAt: p.createdAt,
499
+ country: p.country,
500
+ utmSource: fields.utmSource ?? null,
501
+ utmMedium: fields.utmMedium ?? null,
502
+ utmCampaign: fields.utmCampaign ?? null
503
+ };
504
+ });
505
+ events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
506
+ return {
507
+ user: userRecord,
508
+ events,
509
+ purchases: purchaseResults
510
+ };
511
+ }
512
+ __name(traceAttribution, "traceAttribution");
513
+ return {
514
+ getRevenueSummary,
515
+ getRevenueByDay,
516
+ getPreviousPeriodRevenueByDay,
517
+ getRevenueByProduct,
518
+ getRevenueByCountry,
519
+ getRecentPurchases,
520
+ getAttributionSummary,
521
+ getShortlinkPerformance,
522
+ getRevenueBySource,
523
+ getConversionFunnel,
524
+ getAttributedRevenueSummary,
525
+ getContentPurchaseCorrelation,
526
+ traceAttribution
527
+ };
528
+ }
529
+ __name(createDatabaseProvider, "createDatabaseProvider");
530
+ export {
531
+ createDatabaseProvider
532
+ };
533
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/database.ts"],"sourcesContent":["import {\n\tand,\n\tcount,\n\tdesc,\n\teq,\n\tgt,\n\tgte,\n\tinArray,\n\tlte,\n\tsql,\n\tsum,\n} from 'drizzle-orm'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AnalyticsTimeRange = '24h' | '7d' | '30d' | '90d' | 'all'\n\nexport interface AttributionTrailEvent {\n\ttype: 'click' | 'signup' | 'progress' | 'purchase'\n\ttimestamp: Date\n\tdetail: Record<string, any>\n}\n\nexport interface AttributionTrail {\n\tuser: {\n\t\tid: string\n\t\temail: string | null\n\t\tname: string | null\n\t\tcreatedAt: Date\n\t} | null\n\tevents: AttributionTrailEvent[]\n\tpurchases: {\n\t\tid: string\n\t\ttotalAmount: number\n\t\tproductName: string\n\t\tcreatedAt: Date\n\t\tcountry: string | null\n\t\tutmSource: string | null\n\t\tutmMedium: string | null\n\t\tutmCampaign: string | null\n\t}[]\n}\n\n// ─── Schema type ─────────────────────────────────────────────────────────────\n\nexport interface DatabaseAnalyticsSchema {\n\tpurchases: any\n\tproducts: any\n\tusers: any\n\tcoupon: any\n\tresourceProgress: any\n\tshortlink: any\n\tshortlinkAttribution: any\n\tshortlinkClick: any\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a database analytics provider that wraps all analytics query\n * functions with an injected drizzle db instance and schema tables.\n *\n * @param db - Drizzle database instance\n * @param schema - Object containing the required table references\n */\nexport function createDatabaseProvider(\n\tdb: any,\n\tschema: DatabaseAnalyticsSchema,\n) {\n\tconst {\n\t\tpurchases,\n\t\tproducts,\n\t\tusers,\n\t\tcoupon,\n\t\tresourceProgress,\n\t\tshortlink,\n\t\tshortlinkAttribution,\n\t\tshortlinkClick,\n\t} = schema\n\n\t// ─── Internal helpers ───────────────────────────────────────────────────\n\n\tconst PAID_STATUSES = ['Valid', 'Restricted'] as const\n\n\tfunction paidPurchase() {\n\t\treturn inArray(purchases.status, [...PAID_STATUSES])\n\t}\n\n\tfunction rangeToDate(range: AnalyticsTimeRange): Date | null {\n\t\tif (range === 'all') return null\n\t\tconst now = new Date()\n\t\tconst hours: Record<string, number> = {\n\t\t\t'24h': 24,\n\t\t\t'7d': 7 * 24,\n\t\t\t'30d': 30 * 24,\n\t\t\t'90d': 90 * 24,\n\t\t}\n\t\treturn new Date(now.getTime() - (hours[range] ?? 30 * 24) * 60 * 60 * 1000)\n\t}\n\n\t// ─── Revenue ───────────────────────────────────────────────────────────\n\n\tasync function getRevenueSummary(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [totals] = await db\n\t\t\t.select({\n\t\t\t\ttotalRevenue: sum(purchases.totalAmount),\n\t\t\t\tpurchaseCount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\n\t\treturn {\n\t\t\ttotalRevenue: Number(totals?.totalRevenue ?? 0),\n\t\t\tpurchaseCount: totals?.purchaseCount ?? 0,\n\t\t\tavgOrderValue:\n\t\t\t\ttotals?.purchaseCount && totals.purchaseCount > 0\n\t\t\t\t\t? Number(totals.totalRevenue ?? 0) / totals.purchaseCount\n\t\t\t\t\t: 0,\n\t\t}\n\t}\n\n\tasync function getRevenueByDay(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tdate: sql<string>`DATE(${purchases.createdAt})`.as('date'),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(sql`DATE(${purchases.createdAt})`)\n\t\t\t.orderBy(sql`DATE(${purchases.createdAt})`)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tdate: r.date,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\t/**\n\t * Revenue by day for the previous period of equal length.\n\t * E.g., if range = '30d', returns the 30 days before those 30 days.\n\t * Returns data with a `dayOffset` (0 = start of period) for overlay\n\t * alignment.\n\t */\n\tasync function getPreviousPeriodRevenueByDay(\n\t\trange: AnalyticsTimeRange = '30d',\n\t) {\n\t\tif (range === 'all') return []\n\n\t\tconst hours: Record<string, number> = {\n\t\t\t'24h': 24,\n\t\t\t'7d': 7 * 24,\n\t\t\t'30d': 30 * 24,\n\t\t\t'90d': 90 * 24,\n\t\t}\n\t\tconst periodMs = (hours[range] ?? 30 * 24) * 60 * 60 * 1000\n\t\tconst now = new Date()\n\t\tconst periodStart = new Date(now.getTime() - periodMs)\n\t\tconst prevStart = new Date(periodStart.getTime() - periodMs)\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tdate: sql<string>`DATE(${purchases.createdAt})`.as('date'),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\tpaidPurchase(),\n\t\t\t\t\tgte(purchases.createdAt, prevStart),\n\t\t\t\t\tlte(purchases.createdAt, periodStart),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.groupBy(sql`DATE(${purchases.createdAt})`)\n\t\t\t.orderBy(sql`DATE(${purchases.createdAt})`)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tdate: r.date,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRevenueByProduct(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tproductId: purchases.productId,\n\t\t\t\tproductName: products.name,\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(purchases.productId, products.name)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tproductId: r.productId,\n\t\t\tproductName: r.productName ?? '(unknown)',\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRevenueByCountry(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tcountry: purchases.country,\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(purchases.country)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\t\t\t.limit(20)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tcountry: r.country ?? '(unknown)',\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRecentPurchases(\n\t\tlimit: number = 20,\n\t\tfilter: 'all' | 'team' | 'individual' = 'all',\n\t\trange: AnalyticsTimeRange = 'all',\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tif (filter === 'team') {\n\t\t\t// Multi-seat purchases: join coupon to filter seats > 1, sort by amount\n\t\t\tconditions.push(sql`${purchases.bulkCouponId} IS NOT NULL`)\n\n\t\t\tconst rows = await db\n\t\t\t\t.select({\n\t\t\t\t\tid: purchases.id,\n\t\t\t\t\tcreatedAt: purchases.createdAt,\n\t\t\t\t\ttotalAmount: purchases.totalAmount,\n\t\t\t\t\tproductName: products.name,\n\t\t\t\t\tproductId: purchases.productId,\n\t\t\t\t\tcountry: purchases.country,\n\t\t\t\t\tcouponId: purchases.couponId,\n\t\t\t\t\tuserId: purchases.userId,\n\t\t\t\t\tuserName: users.name,\n\t\t\t\t\tuserEmail: users.email,\n\t\t\t\t\torganizationId: purchases.organizationId,\n\t\t\t\t\tseats: coupon.maxUses,\n\t\t\t\t})\n\t\t\t\t.from(purchases)\n\t\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t\t.leftJoin(users, eq(purchases.userId, users.id))\n\t\t\t\t.leftJoin(coupon, eq(purchases.bulkCouponId, coupon.id))\n\t\t\t\t.where(and(...conditions, gt(coupon.maxUses, 1)))\n\t\t\t\t.orderBy(desc(purchases.totalAmount))\n\t\t\t\t.limit(limit)\n\n\t\t\treturn rows.map((r: any) => ({\n\t\t\t\tid: r.id,\n\t\t\t\tcreatedAt: r.createdAt,\n\t\t\t\ttotalAmount: Number(r.totalAmount),\n\t\t\t\tproductName: r.productName ?? '(unknown)',\n\t\t\t\tproductId: r.productId,\n\t\t\t\tcountry: r.country,\n\t\t\t\tcouponId: r.couponId,\n\t\t\t\tuserName: r.userName ?? null,\n\t\t\t\tuserEmail: r.userEmail ?? null,\n\t\t\t\tisTeam: true,\n\t\t\t\tseats: r.seats ?? null,\n\t\t\t}))\n\t\t}\n\n\t\tif (filter === 'individual') {\n\t\t\tconditions.push(sql`${purchases.bulkCouponId} IS NULL`)\n\t\t}\n\n\t\tconst rows = await db.query.purchases.findMany({\n\t\t\twhere: and(...conditions),\n\t\t\torderBy: [desc(purchases.totalAmount)],\n\t\t\tlimit,\n\t\t\twith: {\n\t\t\t\tproduct: true,\n\t\t\t\tuser: true,\n\t\t\t},\n\t\t})\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tid: r.id,\n\t\t\tcreatedAt: r.createdAt,\n\t\t\ttotalAmount: Number(r.totalAmount),\n\t\t\tproductName: r.product?.name ?? '(unknown)',\n\t\t\tproductId: r.productId,\n\t\t\tcountry: r.country,\n\t\t\tcouponId: r.couponId,\n\t\t\tuserName: r.user?.name ?? null,\n\t\t\tuserEmail: r.user?.email ?? null,\n\t\t\tisTeam: r.organizationId != null,\n\t\t\tseats: null as number | null,\n\t\t}))\n\t}\n\n\t// ─── Attribution ────────────────────────────────────────────────────────\n\n\tasync function getAttributionSummary(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions: any[] = []\n\t\tif (since) conditions.push(gte(shortlinkAttribution.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(shortlinkAttribution)\n\t\t\t.where(conditions.length > 0 ? and(...conditions) : undefined)\n\t\t\t.groupBy(shortlinkAttribution.type)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\ttype: r.type,\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getShortlinkPerformance(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst clickConditions: any[] = []\n\t\tif (since) clickConditions.push(gte(shortlinkClick.timestamp, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tshortlinkId: shortlinkClick.shortlinkId,\n\t\t\t\tslug: shortlink.slug,\n\t\t\t\turl: shortlink.url,\n\t\t\t\tclicks: count(),\n\t\t\t})\n\t\t\t.from(shortlinkClick)\n\t\t\t.innerJoin(shortlink, eq(shortlinkClick.shortlinkId, shortlink.id))\n\t\t\t.where(clickConditions.length > 0 ? and(...clickConditions) : undefined)\n\t\t\t.groupBy(shortlinkClick.shortlinkId, shortlink.slug, shortlink.url)\n\t\t\t.orderBy(desc(count()))\n\t\t\t.limit(20)\n\n\t\t// Get attribution counts per shortlink\n\t\tconst attrConditions: any[] = []\n\t\tif (since) attrConditions.push(gte(shortlinkAttribution.createdAt, since))\n\n\t\tconst attrRows = await db\n\t\t\t.select({\n\t\t\t\tshortlinkId: shortlinkAttribution.shortlinkId,\n\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(shortlinkAttribution)\n\t\t\t.where(attrConditions.length > 0 ? and(...attrConditions) : undefined)\n\t\t\t.groupBy(shortlinkAttribution.shortlinkId, shortlinkAttribution.type)\n\n\t\tconst attrMap = new Map<string, { signups: number; purchases: number }>()\n\t\tfor (const a of attrRows) {\n\t\t\tconst existing = attrMap.get(a.shortlinkId) ?? {\n\t\t\t\tsignups: 0,\n\t\t\t\tpurchases: 0,\n\t\t\t}\n\t\t\tif (a.type === 'signup') existing.signups = a.count\n\t\t\tif (a.type === 'purchase') existing.purchases = a.count\n\t\t\tattrMap.set(a.shortlinkId, existing)\n\t\t}\n\n\t\treturn rows.map((r: any) => {\n\t\t\tconst attr = attrMap.get(r.shortlinkId)\n\t\t\treturn {\n\t\t\t\tshortlinkId: r.shortlinkId,\n\t\t\t\tslug: r.slug,\n\t\t\t\turl: r.url,\n\t\t\t\tclicks: r.clicks,\n\t\t\t\tsignups: attr?.signups ?? 0,\n\t\t\t\tpurchases: attr?.purchases ?? 0,\n\t\t\t}\n\t\t})\n\t}\n\n\t// ─── Revenue Attribution ─────────────────────────────────────────────────\n\n\tasync function getRevenueBySource(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tsource:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`.as(\n\t\t\t\t\t\t'source',\n\t\t\t\t\t),\n\t\t\t\tmedium:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`.as(\n\t\t\t\t\t\t'medium',\n\t\t\t\t\t),\n\t\t\t\tcampaign:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`.as(\n\t\t\t\t\t\t'campaign',\n\t\t\t\t\t),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`,\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`,\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`,\n\t\t\t)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tsource: r.source ?? null,\n\t\t\tmedium: r.medium ?? null,\n\t\t\tcampaign: r.campaign ?? null,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getConversionFunnel(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\n\t\tconst userConditions = since ? [gte(users.createdAt, since)] : []\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (since) purchaseConditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [userCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(users)\n\t\t\t.where(userConditions.length > 0 ? and(...userConditions) : undefined)\n\n\t\tconst [purchaseCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...purchaseConditions))\n\n\t\tconst [attributedCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\t...purchaseConditions,\n\t\t\t\t\tsql`(\n JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL\n OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL\n )`,\n\t\t\t\t),\n\t\t\t)\n\n\t\tconst totalSignups = userCount?.total ?? 0\n\t\tconst totalPurchases = purchaseCount?.total ?? 0\n\t\tconst attributedPurchases = attributedCount?.total ?? 0\n\n\t\treturn {\n\t\t\ttotalSignups,\n\t\t\ttotalPurchases,\n\t\t\tattributedPurchases,\n\t\t\tconversionRate: totalSignups > 0 ? totalPurchases / totalSignups : 0,\n\t\t\tattributionCoverage:\n\t\t\t\ttotalPurchases > 0 ? attributedPurchases / totalPurchases : 0,\n\t\t}\n\t}\n\n\tasync function getAttributedRevenueSummary(\n\t\trange: AnalyticsTimeRange = '30d',\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [totals] = await db\n\t\t\t.select({ total: sum(purchases.totalAmount), count: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\n\t\tconst [attributed] = await db\n\t\t\t.select({ total: sum(purchases.totalAmount) })\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\t...conditions,\n\t\t\t\t\tsql`(\n JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL\n OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL\n )`,\n\t\t\t\t),\n\t\t\t)\n\n\t\tconst totalRevenue = Number(totals?.total ?? 0)\n\t\tconst attributedRevenue = Number(attributed?.total ?? 0)\n\t\tconst unattributedRevenue = totalRevenue - attributedRevenue\n\t\tconst totalPurchases = totals?.count ?? 0\n\n\t\treturn {\n\t\t\ttotalRevenue,\n\t\t\tattributedRevenue,\n\t\t\tunattributedRevenue,\n\t\t\tattributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,\n\t\t\ttotalPurchases,\n\t\t}\n\t}\n\n\tasync function getContentPurchaseCorrelation(\n\t\trange: AnalyticsTimeRange = '30d',\n\t\tlimit: number = 20,\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (since) purchaseConditions.push(gte(purchases.createdAt, since))\n\n\t\tconst purchaserRows = await db\n\t\t\t.selectDistinct({ userId: purchases.userId })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...purchaseConditions))\n\n\t\tconst purchaserIds = purchaserRows\n\t\t\t.map((r: any) => r.userId)\n\t\t\t.filter((id: any): id is string => id !== null)\n\n\t\tif (purchaserIds.length === 0) return []\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tresourceId: resourceProgress.resourceId,\n\t\t\t\tpurchaserCount: count(),\n\t\t\t})\n\t\t\t.from(resourceProgress)\n\t\t\t.where(\n\t\t\t\tsql`${resourceProgress.userId} IN (${sql.join(\n\t\t\t\t\tpurchaserIds.map((id: string) => sql`${id}`),\n\t\t\t\t\tsql`, `,\n\t\t\t\t)})`,\n\t\t\t)\n\t\t\t.groupBy(resourceProgress.resourceId)\n\t\t\t.orderBy(desc(count()))\n\t\t\t.limit(limit)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tresourceId: r.resourceId,\n\t\t\tpurchaserCount: r.purchaserCount,\n\t\t}))\n\t}\n\n\t// ─── Attribution Trail ───────────────────────────────────────────────────\n\n\t/**\n\t * Trace the full attribution journey for a user by email or purchaseId.\n\t * Walks: ShortlinkClick → ShortlinkAttribution (signup) →\n\t * ResourceProgress → Purchase\n\t */\n\tasync function traceAttribution(opts: {\n\t\temail?: string\n\t\tpurchaseId?: string\n\t}): Promise<AttributionTrail> {\n\t\tconst events: AttributionTrailEvent[] = []\n\n\t\t// Resolve user\n\t\tlet userId: string | null = null\n\t\tlet userEmail: string | null = opts.email ?? null\n\t\tlet userRecord: AttributionTrail['user'] = null\n\n\t\tif (opts.purchaseId) {\n\t\t\tconst [purchase] = await db\n\t\t\t\t.select({\n\t\t\t\t\tuserId: purchases.userId,\n\t\t\t\t\temail: users.email,\n\t\t\t\t})\n\t\t\t\t.from(purchases)\n\t\t\t\t.leftJoin(users, eq(purchases.userId, users.id))\n\t\t\t\t.where(eq(purchases.id, opts.purchaseId))\n\t\t\t\t.limit(1)\n\t\t\tuserId = purchase?.userId ?? null\n\t\t\tuserEmail = purchase?.email ?? userEmail\n\t\t}\n\n\t\tif (userEmail && !userId) {\n\t\t\tconst [u] = await db\n\t\t\t\t.select({ id: users.id })\n\t\t\t\t.from(users)\n\t\t\t\t.where(eq(users.email, userEmail))\n\t\t\t\t.limit(1)\n\t\t\tuserId = u?.id ?? null\n\t\t}\n\n\t\tif (userId) {\n\t\t\tconst [u] = await db\n\t\t\t\t.select({\n\t\t\t\t\tid: users.id,\n\t\t\t\t\temail: users.email,\n\t\t\t\t\tname: users.name,\n\t\t\t\t\tcreatedAt: users.createdAt,\n\t\t\t\t})\n\t\t\t\t.from(users)\n\t\t\t\t.where(eq(users.id, userId))\n\t\t\t\t.limit(1)\n\t\t\tuserRecord = u\n\t\t\t\t? {\n\t\t\t\t\t\tid: u.id,\n\t\t\t\t\t\temail: u.email,\n\t\t\t\t\t\tname: u.name ?? null,\n\t\t\t\t\t\tcreatedAt: u.createdAt!,\n\t\t\t\t\t}\n\t\t\t\t: null\n\t\t}\n\n\t\t// Find shortlink attributions for this user (by userId or email)\n\t\tconst attrConditions: any[] = []\n\t\tif (userId) attrConditions.push(eq(shortlinkAttribution.userId, userId))\n\t\tif (userEmail)\n\t\t\tattrConditions.push(eq(shortlinkAttribution.email, userEmail))\n\n\t\tif (attrConditions.length > 0) {\n\t\t\tconst attrs = await db\n\t\t\t\t.select({\n\t\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\t\tcreatedAt: shortlinkAttribution.createdAt,\n\t\t\t\t\tmetadata: shortlinkAttribution.metadata,\n\t\t\t\t\tshortlinkId: shortlinkAttribution.shortlinkId,\n\t\t\t\t\tslug: shortlink.slug,\n\t\t\t\t\turl: shortlink.url,\n\t\t\t\t})\n\t\t\t\t.from(shortlinkAttribution)\n\t\t\t\t.leftJoin(shortlink, eq(shortlinkAttribution.shortlinkId, shortlink.id))\n\t\t\t\t.where(sql`(${sql.join(attrConditions, sql` OR `)})`)\n\t\t\t\t.orderBy(shortlinkAttribution.createdAt)\n\n\t\t\tfor (const attr of attrs) {\n\t\t\t\t// Find clicks on this shortlink before the attribution event\n\t\t\t\tconst clicks = await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\ttimestamp: shortlinkClick.timestamp,\n\t\t\t\t\t\treferrer: shortlinkClick.referrer,\n\t\t\t\t\t\tcountry: shortlinkClick.country,\n\t\t\t\t\t\tdevice: shortlinkClick.device,\n\t\t\t\t\t})\n\t\t\t\t\t.from(shortlinkClick)\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(\n\t\t\t\t\t\t\teq(shortlinkClick.shortlinkId, attr.shortlinkId),\n\t\t\t\t\t\t\tlte(shortlinkClick.timestamp, attr.createdAt),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t\t.orderBy(desc(shortlinkClick.timestamp))\n\t\t\t\t\t.limit(3) // last 3 clicks before attribution\n\n\t\t\t\tfor (const click of clicks) {\n\t\t\t\t\tevents.push({\n\t\t\t\t\t\ttype: 'click',\n\t\t\t\t\t\ttimestamp: click.timestamp,\n\t\t\t\t\t\tdetail: {\n\t\t\t\t\t\t\tshortlink: `/s/${attr.slug}`,\n\t\t\t\t\t\t\tdestination: attr.url,\n\t\t\t\t\t\t\treferrer: click.referrer,\n\t\t\t\t\t\t\tcountry: click.country,\n\t\t\t\t\t\t\tdevice: click.device,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tevents.push({\n\t\t\t\t\ttype: attr.type === 'purchase' ? 'purchase' : 'signup',\n\t\t\t\t\ttimestamp: attr.createdAt,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tshortlink: `/s/${attr.slug}`,\n\t\t\t\t\t\tmetadata: attr.metadata ? JSON.parse(attr.metadata) : null,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Find resource progress for this user\n\t\tif (userId) {\n\t\t\tconst progress = await db\n\t\t\t\t.select({\n\t\t\t\t\tresourceId: resourceProgress.resourceId,\n\t\t\t\t\tcompletedAt: resourceProgress.completedAt,\n\t\t\t\t\tcreatedAt: resourceProgress.createdAt,\n\t\t\t\t})\n\t\t\t\t.from(resourceProgress)\n\t\t\t\t.where(eq(resourceProgress.userId, userId))\n\t\t\t\t.orderBy(resourceProgress.createdAt)\n\t\t\t\t.limit(20) // cap at 20 most recent\n\n\t\t\tfor (const p of progress) {\n\t\t\t\tevents.push({\n\t\t\t\t\ttype: 'progress',\n\t\t\t\t\ttimestamp: p.completedAt ?? p.createdAt,\n\t\t\t\t\tdetail: { resourceId: p.resourceId },\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Find purchases\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (opts.purchaseId) {\n\t\t\tpurchaseConditions.push(eq(purchases.id, opts.purchaseId))\n\t\t} else if (userId) {\n\t\t\tpurchaseConditions.push(eq(purchases.userId, userId))\n\t\t} else {\n\t\t\t// No user found, return empty\n\t\t\treturn { user: userRecord, events: [], purchases: [] }\n\t\t}\n\n\t\tconst purchaseRows = await db\n\t\t\t.select({\n\t\t\t\tid: purchases.id,\n\t\t\t\ttotalAmount: purchases.totalAmount,\n\t\t\t\tproductName: products.name,\n\t\t\t\tcreatedAt: purchases.createdAt,\n\t\t\t\tcountry: purchases.country,\n\t\t\t\tfields: purchases.fields,\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t.where(and(...purchaseConditions))\n\t\t\t.orderBy(purchases.createdAt)\n\n\t\tconst purchaseResults = purchaseRows.map((p: any) => {\n\t\t\tconst fields = (p.fields as Record<string, any>) ?? {}\n\t\t\tevents.push({\n\t\t\t\ttype: 'purchase',\n\t\t\t\ttimestamp: p.createdAt,\n\t\t\t\tdetail: {\n\t\t\t\t\tpurchaseId: p.id,\n\t\t\t\t\tamount: Number(p.totalAmount),\n\t\t\t\t\tproduct: p.productName,\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tid: p.id,\n\t\t\t\ttotalAmount: Number(p.totalAmount),\n\t\t\t\tproductName: p.productName ?? 'Unknown',\n\t\t\t\tcreatedAt: p.createdAt,\n\t\t\t\tcountry: p.country,\n\t\t\t\tutmSource: fields.utmSource ?? null,\n\t\t\t\tutmMedium: fields.utmMedium ?? null,\n\t\t\t\tutmCampaign: fields.utmCampaign ?? null,\n\t\t\t}\n\t\t})\n\n\t\t// Sort all events by timestamp\n\t\tevents.sort(\n\t\t\t(a, b) =>\n\t\t\t\tnew Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),\n\t\t)\n\n\t\treturn { user: userRecord, events, purchases: purchaseResults }\n\t}\n\n\treturn {\n\t\tgetRevenueSummary,\n\t\tgetRevenueByDay,\n\t\tgetPreviousPeriodRevenueByDay,\n\t\tgetRevenueByProduct,\n\t\tgetRevenueByCountry,\n\t\tgetRecentPurchases,\n\t\tgetAttributionSummary,\n\t\tgetShortlinkPerformance,\n\t\tgetRevenueBySource,\n\t\tgetConversionFunnel,\n\t\tgetAttributedRevenueSummary,\n\t\tgetContentPurchaseCorrelation,\n\t\ttraceAttribution,\n\t}\n}\n\nexport type DatabaseAnalyticsProvider = ReturnType<\n\ttypeof createDatabaseProvider\n>\n"],"mappings":"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","names":["and","count","desc","eq","gt","gte","inArray","lte","sql","sum","createDatabaseProvider","db","schema","purchases","products","users","coupon","resourceProgress","shortlink","shortlinkAttribution","shortlinkClick","PAID_STATUSES","paidPurchase","inArray","status","rangeToDate","range","now","Date","hours","getTime","getRevenueSummary","since","conditions","push","gte","createdAt","totals","select","totalRevenue","sum","totalAmount","purchaseCount","count","from","where","and","Number","avgOrderValue","getRevenueByDay","rows","date","sql","as","revenue","groupBy","orderBy","map","r","getPreviousPeriodRevenueByDay","periodMs","periodStart","prevStart","lte","getRevenueByProduct","productId","productName","name","leftJoin","eq","id","desc","getRevenueByCountry","country","limit","getRecentPurchases","filter","bulkCouponId","couponId","userId","userName","userEmail","email","organizationId","seats","maxUses","gt","isTeam","query","findMany","with","product","user","getAttributionSummary","type","length","undefined","getShortlinkPerformance","clickConditions","timestamp","shortlinkId","slug","url","clicks","innerJoin","attrConditions","attrRows","attrMap","Map","a","existing","get","signups","set","attr","getRevenueBySource","source","fields","medium","campaign","getConversionFunnel","userConditions","purchaseConditions","userCount","total","attributedCount","totalSignups","totalPurchases","attributedPurchases","conversionRate","attributionCoverage","getAttributedRevenueSummary","attributed","attributedRevenue","unattributedRevenue","attributionRate","getContentPurchaseCorrelation","purchaserRows","selectDistinct","purchaserIds","resourceId","purchaserCount","join","traceAttribution","opts","events","userRecord","purchaseId","purchase","u","attrs","metadata","referrer","device","click","detail","destination","JSON","parse","progress","completedAt","p","purchaseRows","purchaseResults","amount","utmSource","utmMedium","utmCampaign","sort","b"]}
@@ -0,0 +1,45 @@
1
+ import { GA4TimeRange } from './ga4.js';
2
+
3
+ type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all';
4
+ interface TrafficRevenueCorrelation {
5
+ traffic: {
6
+ date: string;
7
+ sessions: number;
8
+ users: number;
9
+ pageviews: number;
10
+ }[];
11
+ revenue: {
12
+ date: string;
13
+ revenue: number;
14
+ count: number;
15
+ }[];
16
+ }
17
+ interface DerivedProviderDeps {
18
+ database: {
19
+ getRevenueByDay: (range: AnalyticsRange) => Promise<{
20
+ date: string;
21
+ revenue: number;
22
+ count: number;
23
+ }[]>;
24
+ };
25
+ ga4: {
26
+ getSessionsByDay: (range: GA4TimeRange) => Promise<{
27
+ date: string;
28
+ sessions: number;
29
+ users: number;
30
+ pageviews: number;
31
+ }[]>;
32
+ };
33
+ }
34
+ /**
35
+ * Creates a derived analytics provider that combines data from multiple
36
+ * sources (database + GA4) to compute correlation metrics.
37
+ *
38
+ * @param deps - Provider dependencies (database and ga4 providers)
39
+ */
40
+ declare function createDerivedProvider(deps: DerivedProviderDeps): {
41
+ getTrafficRevenueCorrelation: (range: AnalyticsRange) => Promise<TrafficRevenueCorrelation>;
42
+ };
43
+ type DerivedAnalyticsProvider = ReturnType<typeof createDerivedProvider>;
44
+
45
+ export { type AnalyticsRange, type DerivedAnalyticsProvider, type DerivedProviderDeps, type TrafficRevenueCorrelation, createDerivedProvider };