@happyvertical/smrt-ads 0.30.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.
package/dist/index.js ADDED
@@ -0,0 +1,1121 @@
1
+ import { ObjectRegistry, smrt, SmrtObject, SmrtCollection, foreignKey, crossPackageRef } from "@happyvertical/smrt-core";
2
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
3
+ ObjectRegistry.registerPackageManifest(
4
+ new URL("./manifest.json", import.meta.url)
5
+ );
6
+ var AdFormatType = /* @__PURE__ */ ((AdFormatType2) => {
7
+ AdFormatType2["BANNER"] = "banner";
8
+ AdFormatType2["NATIVE"] = "native";
9
+ AdFormatType2["VIDEO"] = "video";
10
+ return AdFormatType2;
11
+ })(AdFormatType || {});
12
+ var PricingModel = /* @__PURE__ */ ((PricingModel2) => {
13
+ PricingModel2["FIXED"] = "fixed";
14
+ PricingModel2["CPM"] = "cpm";
15
+ PricingModel2["CPC"] = "cpc";
16
+ PricingModel2["CPA"] = "cpa";
17
+ return PricingModel2;
18
+ })(PricingModel || {});
19
+ var AdGroupStatus = /* @__PURE__ */ ((AdGroupStatus2) => {
20
+ AdGroupStatus2["DRAFT"] = "draft";
21
+ AdGroupStatus2["ACTIVE"] = "active";
22
+ AdGroupStatus2["PAUSED"] = "paused";
23
+ AdGroupStatus2["COMPLETED"] = "completed";
24
+ return AdGroupStatus2;
25
+ })(AdGroupStatus || {});
26
+ var AdVariationStatus = /* @__PURE__ */ ((AdVariationStatus2) => {
27
+ AdVariationStatus2["DRAFT"] = "draft";
28
+ AdVariationStatus2["ACTIVE"] = "active";
29
+ AdVariationStatus2["PAUSED"] = "paused";
30
+ return AdVariationStatus2;
31
+ })(AdVariationStatus || {});
32
+ var AdEventType = /* @__PURE__ */ ((AdEventType2) => {
33
+ AdEventType2["IMPRESSION"] = "impression";
34
+ AdEventType2["CLICK"] = "click";
35
+ AdEventType2["CONVERSION"] = "conversion";
36
+ return AdEventType2;
37
+ })(AdEventType || {});
38
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
39
+ var __decorateClass$4 = (decorators, target, key, kind) => {
40
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
41
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
42
+ if (decorator = decorators[i])
43
+ result = decorator(result) || result;
44
+ return result;
45
+ };
46
+ let AdDeliveryTier = class extends SmrtObject {
47
+ /**
48
+ * Display name (e.g., "Sponsorship", "Standard", "House")
49
+ */
50
+ name = "";
51
+ /**
52
+ * Priority level (lower = higher priority: 1, 2, 3...)
53
+ */
54
+ priority = 0;
55
+ /**
56
+ * Pricing model for this tier
57
+ */
58
+ pricingModel = PricingModel.CPM;
59
+ /**
60
+ * Optional description
61
+ */
62
+ description = "";
63
+ constructor(options = {}) {
64
+ super(options);
65
+ if (options.name !== void 0) this.name = options.name;
66
+ if (options.priority !== void 0) this.priority = options.priority;
67
+ if (options.pricingModel !== void 0)
68
+ this.pricingModel = options.pricingModel;
69
+ if (options.description !== void 0)
70
+ this.description = options.description;
71
+ }
72
+ /**
73
+ * Check if this tier is higher priority than another
74
+ */
75
+ isHigherPriorityThan(other) {
76
+ return this.priority < other.priority;
77
+ }
78
+ /**
79
+ * Check if this is a fixed pricing tier
80
+ */
81
+ isFixedPricing() {
82
+ return this.pricingModel === PricingModel.FIXED;
83
+ }
84
+ /**
85
+ * Check if this is a performance-based tier (CPC, CPA)
86
+ */
87
+ isPerformanceBased() {
88
+ return this.pricingModel === PricingModel.CPC || this.pricingModel === PricingModel.CPA;
89
+ }
90
+ };
91
+ AdDeliveryTier = __decorateClass$4([
92
+ smrt({
93
+ api: { include: ["list", "get", "create", "update"] },
94
+ mcp: { include: ["list", "get"] },
95
+ cli: true
96
+ })
97
+ ], AdDeliveryTier);
98
+ class AdDeliveryTierCollection extends SmrtCollection {
99
+ static _itemClass = AdDeliveryTier;
100
+ /**
101
+ * Find tiers ordered by priority (ascending, lower = higher priority)
102
+ *
103
+ * @returns Array of tiers ordered by priority
104
+ */
105
+ async findByPriority() {
106
+ return await this.list({
107
+ orderBy: "priority ASC"
108
+ });
109
+ }
110
+ /**
111
+ * Find tiers by pricing model
112
+ *
113
+ * @param pricingModel - Pricing model to filter by
114
+ * @returns Array of matching tiers
115
+ */
116
+ async findByPricingModel(pricingModel) {
117
+ return await this.list({
118
+ where: { pricingModel },
119
+ orderBy: "priority ASC"
120
+ });
121
+ }
122
+ /**
123
+ * Get the highest priority tier
124
+ *
125
+ * @returns Highest priority tier or null
126
+ */
127
+ async getHighestPriority() {
128
+ const results = await this.list({
129
+ orderBy: "priority ASC",
130
+ limit: 1
131
+ });
132
+ return results.length > 0 ? results[0] : null;
133
+ }
134
+ /**
135
+ * Find fixed pricing tiers
136
+ */
137
+ async findFixedPricing() {
138
+ return await this.findByPricingModel(PricingModel.FIXED);
139
+ }
140
+ /**
141
+ * Find CPM-based tiers
142
+ */
143
+ async findCPM() {
144
+ return await this.findByPricingModel(PricingModel.CPM);
145
+ }
146
+ /**
147
+ * Find performance-based tiers (CPC, CPA)
148
+ */
149
+ async findPerformanceBased() {
150
+ const cpc = await this.findByPricingModel(PricingModel.CPC);
151
+ const cpa = await this.findByPricingModel(PricingModel.CPA);
152
+ return [...cpc, ...cpa].sort((a, b) => a.priority - b.priority);
153
+ }
154
+ }
155
+ var __defProp$2 = Object.defineProperty;
156
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
157
+ var __decorateClass$3 = (decorators, target, key, kind) => {
158
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
159
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
160
+ if (decorator = decorators[i])
161
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
162
+ if (kind && result) __defProp$2(target, key, result);
163
+ return result;
164
+ };
165
+ let AdEvent = class extends SmrtObject {
166
+ tenantId = null;
167
+ variationId = "";
168
+ zoneId = "";
169
+ /**
170
+ * Site ID (denormalized from Zone for query efficiency)
171
+ */
172
+ siteId = "";
173
+ /**
174
+ * Event type (impression, click, conversion)
175
+ */
176
+ eventType = AdEventType.IMPRESSION;
177
+ /**
178
+ * Event timestamp
179
+ */
180
+ timestamp = /* @__PURE__ */ new Date();
181
+ /**
182
+ * Analytics metadata as JSON string
183
+ * (IP, user agent, referrer, etc.)
184
+ */
185
+ metadata = "";
186
+ constructor(options = {}) {
187
+ super(options);
188
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
189
+ if (options.variationId !== void 0)
190
+ this.variationId = options.variationId;
191
+ if (options.zoneId !== void 0) this.zoneId = options.zoneId;
192
+ if (options.siteId !== void 0) this.siteId = options.siteId;
193
+ if (options.eventType !== void 0) this.eventType = options.eventType;
194
+ if (options.timestamp !== void 0) this.timestamp = options.timestamp;
195
+ if (options.metadata !== void 0) this.metadata = options.metadata;
196
+ }
197
+ /**
198
+ * Get metadata as object
199
+ */
200
+ getMetadata() {
201
+ if (!this.metadata) return {};
202
+ try {
203
+ return JSON.parse(this.metadata);
204
+ } catch {
205
+ return {};
206
+ }
207
+ }
208
+ /**
209
+ * Set metadata from object
210
+ */
211
+ setMetadata(data) {
212
+ this.metadata = JSON.stringify(data);
213
+ }
214
+ /**
215
+ * Check if this is an impression event
216
+ */
217
+ isImpression() {
218
+ return this.eventType === AdEventType.IMPRESSION;
219
+ }
220
+ /**
221
+ * Check if this is a click event
222
+ */
223
+ isClick() {
224
+ return this.eventType === AdEventType.CLICK;
225
+ }
226
+ /**
227
+ * Check if this is a conversion event
228
+ */
229
+ isConversion() {
230
+ return this.eventType === AdEventType.CONVERSION;
231
+ }
232
+ };
233
+ __decorateClass$3([
234
+ tenantId({ nullable: true })
235
+ ], AdEvent.prototype, "tenantId", 2);
236
+ __decorateClass$3([
237
+ foreignKey("AdVariation")
238
+ ], AdEvent.prototype, "variationId", 2);
239
+ __decorateClass$3([
240
+ crossPackageRef("@happyvertical/smrt-properties:Zone")
241
+ ], AdEvent.prototype, "zoneId", 2);
242
+ AdEvent = __decorateClass$3([
243
+ TenantScoped({ mode: "optional" }),
244
+ smrt({
245
+ tableStrategy: "sti",
246
+ api: { include: ["create", "list"] },
247
+ // No update/delete (immutable)
248
+ mcp: { include: ["create"] },
249
+ cli: false
250
+ // High volume, not useful in CLI
251
+ })
252
+ ], AdEvent);
253
+ class AdEventCollection extends SmrtCollection {
254
+ static _itemClass = AdEvent;
255
+ /**
256
+ * Find events by variation
257
+ *
258
+ * @param variationId - Variation ID
259
+ * @returns Array of events
260
+ */
261
+ async findByVariation(variationId) {
262
+ return await this.list({
263
+ where: { variationId },
264
+ orderBy: "timestamp DESC"
265
+ });
266
+ }
267
+ /**
268
+ * Find events by zone
269
+ *
270
+ * @param zoneId - Zone ID
271
+ * @returns Array of events
272
+ */
273
+ async findByZone(zoneId) {
274
+ return await this.list({
275
+ where: { zoneId },
276
+ orderBy: "timestamp DESC"
277
+ });
278
+ }
279
+ /**
280
+ * Find events by site
281
+ *
282
+ * @param siteId - Site ID
283
+ * @returns Array of events
284
+ */
285
+ async findBySite(siteId) {
286
+ return await this.list({
287
+ where: { siteId },
288
+ orderBy: "timestamp DESC"
289
+ });
290
+ }
291
+ /**
292
+ * Find events in date range
293
+ *
294
+ * @param start - Start date
295
+ * @param end - End date
296
+ * @returns Array of events
297
+ */
298
+ async findByDateRange(start, end) {
299
+ return await this.list({
300
+ where: {
301
+ "timestamp >=": start.toISOString(),
302
+ "timestamp <=": end.toISOString()
303
+ },
304
+ orderBy: "timestamp DESC"
305
+ });
306
+ }
307
+ /**
308
+ * Find events by type
309
+ *
310
+ * @param eventType - Event type
311
+ * @returns Array of events
312
+ */
313
+ async findByType(eventType) {
314
+ return await this.list({
315
+ where: { eventType },
316
+ orderBy: "timestamp DESC"
317
+ });
318
+ }
319
+ /**
320
+ * Count events by type for a variation
321
+ *
322
+ * @param variationId - Variation ID
323
+ * @param eventType - Event type
324
+ * @returns Count of events
325
+ */
326
+ async countByType(variationId, eventType) {
327
+ const events = await this.list({
328
+ where: { variationId, eventType }
329
+ });
330
+ return events.length;
331
+ }
332
+ /**
333
+ * Count impressions for a variation
334
+ */
335
+ async countImpressions(variationId) {
336
+ return await this.countByType(variationId, AdEventType.IMPRESSION);
337
+ }
338
+ /**
339
+ * Count clicks for a variation
340
+ */
341
+ async countClicks(variationId) {
342
+ return await this.countByType(variationId, AdEventType.CLICK);
343
+ }
344
+ /**
345
+ * Count conversions for a variation
346
+ */
347
+ async countConversions(variationId) {
348
+ return await this.countByType(variationId, AdEventType.CONVERSION);
349
+ }
350
+ /**
351
+ * Find all impressions
352
+ */
353
+ async findImpressions() {
354
+ return await this.findByType(AdEventType.IMPRESSION);
355
+ }
356
+ /**
357
+ * Find all clicks
358
+ */
359
+ async findClicks() {
360
+ return await this.findByType(AdEventType.CLICK);
361
+ }
362
+ /**
363
+ * Find all conversions
364
+ */
365
+ async findConversions() {
366
+ return await this.findByType(AdEventType.CONVERSION);
367
+ }
368
+ /**
369
+ * Get aggregate stats for a variation
370
+ *
371
+ * @param variationId - Variation ID
372
+ * @returns Stats object with impressions, clicks, conversions, CTR
373
+ */
374
+ async getVariationStats(variationId) {
375
+ const impressions = await this.countImpressions(variationId);
376
+ const clicks = await this.countClicks(variationId);
377
+ const conversions = await this.countConversions(variationId);
378
+ return {
379
+ impressions,
380
+ clicks,
381
+ conversions,
382
+ ctr: impressions > 0 ? clicks / impressions : 0,
383
+ conversionRate: clicks > 0 ? conversions / clicks : 0
384
+ };
385
+ }
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+ // Tenancy Helper Methods
388
+ // ─────────────────────────────────────────────────────────────────────────────
389
+ /**
390
+ * Find ad events belonging to a specific tenant
391
+ *
392
+ * @param tenantId - Tenant ID to filter by
393
+ * @returns Array of ad events for the tenant
394
+ */
395
+ async findByTenant(tenantId2) {
396
+ return this.list({ where: { tenantId: tenantId2 } });
397
+ }
398
+ /**
399
+ * Find global ad events (no tenant association)
400
+ *
401
+ * @returns Array of global ad events
402
+ */
403
+ async findGlobal() {
404
+ return this.list({ where: { tenantId: null } });
405
+ }
406
+ /**
407
+ * Find ad events for a tenant including global (shared) events
408
+ *
409
+ * @param tenantId - Tenant ID to include
410
+ * @returns Array of tenant-specific and global ad events
411
+ */
412
+ async findWithGlobals(tenantId2) {
413
+ return this.query(
414
+ `SELECT * FROM ad_events WHERE tenant_id = ? OR tenant_id IS NULL`,
415
+ [tenantId2]
416
+ );
417
+ }
418
+ }
419
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
420
+ var __decorateClass$2 = (decorators, target, key, kind) => {
421
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
422
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
423
+ if (decorator = decorators[i])
424
+ result = decorator(result) || result;
425
+ return result;
426
+ };
427
+ let AdFormat = class extends SmrtObject {
428
+ /**
429
+ * Display name (e.g., "Leaderboard", "Medium Rectangle")
430
+ */
431
+ name = "";
432
+ /**
433
+ * Width in pixels
434
+ */
435
+ width = 0;
436
+ /**
437
+ * Height in pixels
438
+ */
439
+ height = 0;
440
+ /**
441
+ * Format type (banner, native, video)
442
+ */
443
+ formatType = AdFormatType.BANNER;
444
+ /**
445
+ * Optional description
446
+ */
447
+ description = "";
448
+ constructor(options = {}) {
449
+ super(options);
450
+ if (options.name !== void 0) this.name = options.name;
451
+ if (options.width !== void 0) this.width = options.width;
452
+ if (options.height !== void 0) this.height = options.height;
453
+ if (options.formatType !== void 0) this.formatType = options.formatType;
454
+ if (options.description !== void 0)
455
+ this.description = options.description;
456
+ }
457
+ /**
458
+ * Get dimensions as string (e.g., "728x90")
459
+ */
460
+ getDimensions() {
461
+ return `${this.width}x${this.height}`;
462
+ }
463
+ /**
464
+ * Check if format matches specific dimensions
465
+ */
466
+ matchesDimensions(width, height) {
467
+ return this.width === width && this.height === height;
468
+ }
469
+ };
470
+ AdFormat = __decorateClass$2([
471
+ smrt({
472
+ api: { include: ["list", "get", "create", "update"] },
473
+ mcp: { include: ["list", "get"] },
474
+ cli: true
475
+ })
476
+ ], AdFormat);
477
+ class AdFormatCollection extends SmrtCollection {
478
+ static _itemClass = AdFormat;
479
+ /**
480
+ * Find format by dimensions
481
+ *
482
+ * @param width - Width in pixels
483
+ * @param height - Height in pixels
484
+ * @returns Matching format or null
485
+ */
486
+ async findByDimensions(width, height) {
487
+ const results = await this.list({
488
+ where: { width, height },
489
+ limit: 1
490
+ });
491
+ return results.length > 0 ? results[0] : null;
492
+ }
493
+ /**
494
+ * Find formats by type
495
+ *
496
+ * @param formatType - Format type (banner, native, video)
497
+ * @returns Array of matching formats
498
+ */
499
+ async findByType(formatType) {
500
+ return await this.list({
501
+ where: { formatType },
502
+ orderBy: "name ASC"
503
+ });
504
+ }
505
+ /**
506
+ * Find all banner formats
507
+ */
508
+ async findBanners() {
509
+ return await this.findByType(AdFormatType.BANNER);
510
+ }
511
+ /**
512
+ * Find all native formats
513
+ */
514
+ async findNative() {
515
+ return await this.findByType(AdFormatType.NATIVE);
516
+ }
517
+ /**
518
+ * Find all video formats
519
+ */
520
+ async findVideo() {
521
+ return await this.findByType(AdFormatType.VIDEO);
522
+ }
523
+ }
524
+ var __defProp$1 = Object.defineProperty;
525
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
526
+ var __decorateClass$1 = (decorators, target, key, kind) => {
527
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
528
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
529
+ if (decorator = decorators[i])
530
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
531
+ if (kind && result) __defProp$1(target, key, result);
532
+ return result;
533
+ };
534
+ let AdGroup = class extends SmrtObject {
535
+ tenantId = null;
536
+ contractId = "";
537
+ tierId = "";
538
+ /**
539
+ * Display name (e.g., "Summer Sale - Desktop")
540
+ */
541
+ name = "";
542
+ /**
543
+ * Vertical/category tag slug from smrt-tags (context="advertising")
544
+ */
545
+ verticalSlug = "";
546
+ /**
547
+ * Targeting rules as JSON string
548
+ */
549
+ targeting = "";
550
+ /**
551
+ * Allowed Zone IDs as JSON array string (FK to smrt-properties Zone)
552
+ */
553
+ zoneIds = "";
554
+ /**
555
+ * Campaign start date
556
+ */
557
+ startDate = null;
558
+ /**
559
+ * Campaign end date
560
+ */
561
+ endDate = null;
562
+ /**
563
+ * Daily budget limit
564
+ */
565
+ dailyBudget = 0;
566
+ /**
567
+ * Total campaign budget
568
+ */
569
+ totalBudget = 0;
570
+ /**
571
+ * Current status
572
+ */
573
+ status = AdGroupStatus.DRAFT;
574
+ constructor(options = {}) {
575
+ super(options);
576
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
577
+ if (options.contractId !== void 0) this.contractId = options.contractId;
578
+ if (options.tierId !== void 0) this.tierId = options.tierId;
579
+ if (options.name !== void 0) this.name = options.name;
580
+ if (options.verticalSlug !== void 0)
581
+ this.verticalSlug = options.verticalSlug;
582
+ if (options.targeting !== void 0) this.targeting = options.targeting;
583
+ if (options.zoneIds !== void 0) this.zoneIds = options.zoneIds;
584
+ if (options.startDate !== void 0) this.startDate = options.startDate;
585
+ if (options.endDate !== void 0) this.endDate = options.endDate;
586
+ if (options.dailyBudget !== void 0)
587
+ this.dailyBudget = options.dailyBudget;
588
+ if (options.totalBudget !== void 0)
589
+ this.totalBudget = options.totalBudget;
590
+ if (options.status !== void 0) this.status = options.status;
591
+ }
592
+ /**
593
+ * Get zone IDs as array
594
+ */
595
+ getZoneIds() {
596
+ if (!this.zoneIds) return [];
597
+ try {
598
+ return JSON.parse(this.zoneIds);
599
+ } catch {
600
+ return [];
601
+ }
602
+ }
603
+ /**
604
+ * Set zone IDs from array
605
+ */
606
+ setZoneIds(ids) {
607
+ this.zoneIds = JSON.stringify(ids);
608
+ }
609
+ /**
610
+ * Add a zone ID
611
+ */
612
+ addZoneId(zoneId) {
613
+ const ids = this.getZoneIds();
614
+ if (!ids.includes(zoneId)) {
615
+ ids.push(zoneId);
616
+ this.setZoneIds(ids);
617
+ }
618
+ }
619
+ /**
620
+ * Remove a zone ID
621
+ */
622
+ removeZoneId(zoneId) {
623
+ const ids = this.getZoneIds().filter((id) => id !== zoneId);
624
+ this.setZoneIds(ids);
625
+ }
626
+ /**
627
+ * Check if zone ID is allowed
628
+ */
629
+ hasZoneId(zoneId) {
630
+ return this.getZoneIds().includes(zoneId);
631
+ }
632
+ /**
633
+ * Get targeting rules as object
634
+ */
635
+ getTargeting() {
636
+ if (!this.targeting) return {};
637
+ try {
638
+ return JSON.parse(this.targeting);
639
+ } catch {
640
+ return {};
641
+ }
642
+ }
643
+ /**
644
+ * Set targeting rules from object
645
+ */
646
+ setTargeting(rules) {
647
+ this.targeting = JSON.stringify(rules);
648
+ }
649
+ /**
650
+ * Check if ad group is currently active
651
+ */
652
+ isActive() {
653
+ if (this.status !== AdGroupStatus.ACTIVE) return false;
654
+ const now = /* @__PURE__ */ new Date();
655
+ if (this.startDate && now < this.startDate) return false;
656
+ if (this.endDate && now > this.endDate) return false;
657
+ return true;
658
+ }
659
+ /**
660
+ * Check if ad group is in draft state
661
+ */
662
+ isDraft() {
663
+ return this.status === AdGroupStatus.DRAFT;
664
+ }
665
+ /**
666
+ * Check if ad group is paused
667
+ */
668
+ isPaused() {
669
+ return this.status === AdGroupStatus.PAUSED;
670
+ }
671
+ /**
672
+ * Check if ad group is completed
673
+ */
674
+ isCompleted() {
675
+ return this.status === AdGroupStatus.COMPLETED;
676
+ }
677
+ /**
678
+ * Check if ad group has ended (past end date)
679
+ */
680
+ hasEnded() {
681
+ if (!this.endDate) return false;
682
+ return /* @__PURE__ */ new Date() > this.endDate;
683
+ }
684
+ /**
685
+ * Check if ad group has started
686
+ */
687
+ hasStarted() {
688
+ if (!this.startDate) return true;
689
+ return /* @__PURE__ */ new Date() >= this.startDate;
690
+ }
691
+ };
692
+ __decorateClass$1([
693
+ tenantId({ nullable: true })
694
+ ], AdGroup.prototype, "tenantId", 2);
695
+ __decorateClass$1([
696
+ crossPackageRef("@happyvertical/smrt-commerce:Contract")
697
+ ], AdGroup.prototype, "contractId", 2);
698
+ __decorateClass$1([
699
+ foreignKey("AdDeliveryTier")
700
+ ], AdGroup.prototype, "tierId", 2);
701
+ AdGroup = __decorateClass$1([
702
+ TenantScoped({ mode: "optional" }),
703
+ smrt({
704
+ tableStrategy: "sti",
705
+ api: { include: ["list", "get", "create", "update"] },
706
+ mcp: { include: ["list", "get", "create"] },
707
+ cli: true
708
+ })
709
+ ], AdGroup);
710
+ class AdGroupCollection extends SmrtCollection {
711
+ static _itemClass = AdGroup;
712
+ /**
713
+ * Find ad groups by contract
714
+ *
715
+ * @param contractId - Contract ID
716
+ * @returns Array of ad groups
717
+ */
718
+ async findByContract(contractId) {
719
+ return await this.list({
720
+ where: { contractId },
721
+ orderBy: "created_at DESC"
722
+ });
723
+ }
724
+ /**
725
+ * Find ad groups by tier
726
+ *
727
+ * @param tierId - Delivery tier ID
728
+ * @returns Array of ad groups
729
+ */
730
+ async findByTier(tierId) {
731
+ return await this.list({
732
+ where: { tierId },
733
+ orderBy: "name ASC"
734
+ });
735
+ }
736
+ /**
737
+ * Find ad groups by status
738
+ *
739
+ * @param status - Ad group status
740
+ * @returns Array of ad groups
741
+ */
742
+ async findByStatus(status) {
743
+ return await this.list({
744
+ where: { status },
745
+ orderBy: "created_at DESC"
746
+ });
747
+ }
748
+ /**
749
+ * Find currently active ad groups
750
+ * (status=active, started, not ended)
751
+ *
752
+ * @returns Array of active ad groups
753
+ */
754
+ async findActive() {
755
+ const results = await this.list({
756
+ where: {
757
+ status: AdGroupStatus.ACTIVE
758
+ },
759
+ orderBy: "created_at DESC"
760
+ });
761
+ return results.filter((group) => group.isActive());
762
+ }
763
+ /**
764
+ * Find ad groups by vertical slug
765
+ *
766
+ * @param verticalSlug - Tag slug from smrt-tags
767
+ * @returns Array of ad groups
768
+ */
769
+ async findByVertical(verticalSlug) {
770
+ return await this.list({
771
+ where: { verticalSlug },
772
+ orderBy: "name ASC"
773
+ });
774
+ }
775
+ /**
776
+ * Find ad groups that can serve to a specific zone
777
+ *
778
+ * @param zoneId - Zone ID to check
779
+ * @returns Array of ad groups containing zone
780
+ */
781
+ async findByZone(zoneId) {
782
+ const all = await this.list({
783
+ where: { status: AdGroupStatus.ACTIVE }
784
+ });
785
+ return all.filter((group) => group.hasZoneId(zoneId));
786
+ }
787
+ /**
788
+ * Find ad groups eligible to serve for a zone
789
+ * (active, has zone, within date range)
790
+ *
791
+ * @param zoneId - Zone ID to check
792
+ * @returns Array of eligible ad groups
793
+ */
794
+ async findEligibleForZone(zoneId) {
795
+ const groups = await this.findByZone(zoneId);
796
+ return groups.filter((group) => group.isActive());
797
+ }
798
+ /**
799
+ * Find all draft ad groups
800
+ */
801
+ async findDrafts() {
802
+ return await this.findByStatus(AdGroupStatus.DRAFT);
803
+ }
804
+ /**
805
+ * Find all paused ad groups
806
+ */
807
+ async findPaused() {
808
+ return await this.findByStatus(AdGroupStatus.PAUSED);
809
+ }
810
+ /**
811
+ * Find all completed ad groups
812
+ */
813
+ async findCompleted() {
814
+ return await this.findByStatus(AdGroupStatus.COMPLETED);
815
+ }
816
+ // ─────────────────────────────────────────────────────────────────────────────
817
+ // Tenancy Helper Methods
818
+ // ─────────────────────────────────────────────────────────────────────────────
819
+ /**
820
+ * Find ad groups belonging to a specific tenant
821
+ *
822
+ * @param tenantId - Tenant ID to filter by
823
+ * @returns Array of ad groups for the tenant
824
+ */
825
+ async findByTenant(tenantId2) {
826
+ return this.list({ where: { tenantId: tenantId2 } });
827
+ }
828
+ /**
829
+ * Find global ad groups (no tenant association)
830
+ *
831
+ * @returns Array of global ad groups
832
+ */
833
+ async findGlobal() {
834
+ return this.list({ where: { tenantId: null } });
835
+ }
836
+ /**
837
+ * Find ad groups for a tenant including global (shared) ad groups
838
+ *
839
+ * @param tenantId - Tenant ID to include
840
+ * @returns Array of tenant-specific and global ad groups
841
+ */
842
+ async findWithGlobals(tenantId2) {
843
+ return this.query(
844
+ `SELECT * FROM ad_groups WHERE tenant_id = ? OR tenant_id IS NULL`,
845
+ [tenantId2]
846
+ );
847
+ }
848
+ }
849
+ var __defProp = Object.defineProperty;
850
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
851
+ var __decorateClass = (decorators, target, key, kind) => {
852
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
853
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
854
+ if (decorator = decorators[i])
855
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
856
+ if (kind && result) __defProp(target, key, result);
857
+ return result;
858
+ };
859
+ let AdVariation = class extends SmrtObject {
860
+ tenantId = null;
861
+ groupId = "";
862
+ formatId = "";
863
+ assetId = "";
864
+ /**
865
+ * Display name (e.g., "Version A - Blue CTA")
866
+ */
867
+ name = "";
868
+ /**
869
+ * Click destination URL
870
+ */
871
+ clickUrl = "";
872
+ /**
873
+ * Accessibility alt text
874
+ */
875
+ altText = "";
876
+ /**
877
+ * A/B testing weight (higher = more likely to be selected)
878
+ */
879
+ weight = 1;
880
+ /**
881
+ * Current status
882
+ */
883
+ status = AdVariationStatus.DRAFT;
884
+ /**
885
+ * Denormalized impression count (updated async)
886
+ */
887
+ impressions = 0;
888
+ /**
889
+ * Denormalized click count (updated async)
890
+ */
891
+ clicks = 0;
892
+ constructor(options = {}) {
893
+ super(options);
894
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
895
+ if (options.groupId !== void 0) this.groupId = options.groupId;
896
+ if (options.formatId !== void 0) this.formatId = options.formatId;
897
+ if (options.assetId !== void 0) this.assetId = options.assetId;
898
+ if (options.name !== void 0) this.name = options.name;
899
+ if (options.clickUrl !== void 0) this.clickUrl = options.clickUrl;
900
+ if (options.altText !== void 0) this.altText = options.altText;
901
+ if (options.weight !== void 0) this.weight = options.weight;
902
+ if (options.status !== void 0) this.status = options.status;
903
+ if (options.impressions !== void 0)
904
+ this.impressions = options.impressions;
905
+ if (options.clicks !== void 0) this.clicks = options.clicks;
906
+ }
907
+ /**
908
+ * Check if variation is active
909
+ */
910
+ isActive() {
911
+ return this.status === AdVariationStatus.ACTIVE;
912
+ }
913
+ /**
914
+ * Check if variation is in draft state
915
+ */
916
+ isDraft() {
917
+ return this.status === AdVariationStatus.DRAFT;
918
+ }
919
+ /**
920
+ * Check if variation is paused
921
+ */
922
+ isPaused() {
923
+ return this.status === AdVariationStatus.PAUSED;
924
+ }
925
+ /**
926
+ * Calculate click-through rate (CTR)
927
+ */
928
+ getCTR() {
929
+ if (this.impressions === 0) return 0;
930
+ return this.clicks / this.impressions;
931
+ }
932
+ /**
933
+ * Increment impression count
934
+ */
935
+ recordImpression() {
936
+ this.impressions += 1;
937
+ }
938
+ /**
939
+ * Increment click count
940
+ */
941
+ recordClick() {
942
+ this.clicks += 1;
943
+ }
944
+ };
945
+ __decorateClass([
946
+ tenantId({ nullable: true })
947
+ ], AdVariation.prototype, "tenantId", 2);
948
+ __decorateClass([
949
+ foreignKey("AdGroup")
950
+ ], AdVariation.prototype, "groupId", 2);
951
+ __decorateClass([
952
+ foreignKey("AdFormat")
953
+ ], AdVariation.prototype, "formatId", 2);
954
+ __decorateClass([
955
+ crossPackageRef("@happyvertical/smrt-assets:Asset")
956
+ ], AdVariation.prototype, "assetId", 2);
957
+ AdVariation = __decorateClass([
958
+ TenantScoped({ mode: "optional" }),
959
+ smrt({
960
+ tableStrategy: "sti",
961
+ api: { include: ["list", "get", "create", "update"] },
962
+ mcp: { include: ["list", "get", "create"] },
963
+ cli: true
964
+ })
965
+ ], AdVariation);
966
+ class AdVariationCollection extends SmrtCollection {
967
+ static _itemClass = AdVariation;
968
+ /**
969
+ * Find variations by ad group
970
+ *
971
+ * @param groupId - Ad group ID
972
+ * @returns Array of variations
973
+ */
974
+ async findByGroup(groupId) {
975
+ return await this.list({
976
+ where: { groupId },
977
+ orderBy: "name ASC"
978
+ });
979
+ }
980
+ /**
981
+ * Find variations by format
982
+ *
983
+ * @param formatId - Ad format ID
984
+ * @returns Array of variations
985
+ */
986
+ async findByFormat(formatId) {
987
+ return await this.list({
988
+ where: { formatId },
989
+ orderBy: "name ASC"
990
+ });
991
+ }
992
+ /**
993
+ * Find active variations for a group
994
+ *
995
+ * @param groupId - Ad group ID
996
+ * @returns Array of active variations
997
+ */
998
+ async findActiveByGroup(groupId) {
999
+ return await this.list({
1000
+ where: {
1001
+ groupId,
1002
+ status: AdVariationStatus.ACTIVE
1003
+ },
1004
+ orderBy: "name ASC"
1005
+ });
1006
+ }
1007
+ /**
1008
+ * Select a variation using weighted random selection
1009
+ * Higher weight = more likely to be selected
1010
+ *
1011
+ * @param groupId - Ad group ID
1012
+ * @returns Selected variation or null if none available
1013
+ */
1014
+ async selectByWeight(groupId) {
1015
+ const active = await this.findActiveByGroup(groupId);
1016
+ if (active.length === 0) return null;
1017
+ if (active.length === 1) return active[0];
1018
+ const totalWeight = active.reduce((sum, v) => sum + v.weight, 0);
1019
+ if (totalWeight <= 0) return active[0];
1020
+ let random = Math.random() * totalWeight;
1021
+ for (const variation of active) {
1022
+ random -= variation.weight;
1023
+ if (random <= 0) {
1024
+ return variation;
1025
+ }
1026
+ }
1027
+ return active[active.length - 1];
1028
+ }
1029
+ /**
1030
+ * Find variations by status
1031
+ *
1032
+ * @param status - Variation status
1033
+ * @returns Array of variations
1034
+ */
1035
+ async findByStatus(status) {
1036
+ return await this.list({
1037
+ where: { status },
1038
+ orderBy: "created_at DESC"
1039
+ });
1040
+ }
1041
+ /**
1042
+ * Find all active variations
1043
+ */
1044
+ async findActive() {
1045
+ return await this.findByStatus(AdVariationStatus.ACTIVE);
1046
+ }
1047
+ /**
1048
+ * Find all draft variations
1049
+ */
1050
+ async findDrafts() {
1051
+ return await this.findByStatus(AdVariationStatus.DRAFT);
1052
+ }
1053
+ /**
1054
+ * Find all paused variations
1055
+ */
1056
+ async findPaused() {
1057
+ return await this.findByStatus(AdVariationStatus.PAUSED);
1058
+ }
1059
+ /**
1060
+ * Find top performing variations by CTR
1061
+ *
1062
+ * @param limit - Maximum number to return
1063
+ * @returns Array of variations sorted by CTR descending
1064
+ */
1065
+ async findTopPerformers(limit = 10) {
1066
+ const all = await this.list({
1067
+ where: { status: AdVariationStatus.ACTIVE }
1068
+ });
1069
+ return all.filter((v) => v.impressions > 0).sort((a, b) => b.getCTR() - a.getCTR()).slice(0, limit);
1070
+ }
1071
+ // ─────────────────────────────────────────────────────────────────────────────
1072
+ // Tenancy Helper Methods
1073
+ // ─────────────────────────────────────────────────────────────────────────────
1074
+ /**
1075
+ * Find ad variations belonging to a specific tenant
1076
+ *
1077
+ * @param tenantId - Tenant ID to filter by
1078
+ * @returns Array of ad variations for the tenant
1079
+ */
1080
+ async findByTenant(tenantId2) {
1081
+ return this.list({ where: { tenantId: tenantId2 } });
1082
+ }
1083
+ /**
1084
+ * Find global ad variations (no tenant association)
1085
+ *
1086
+ * @returns Array of global ad variations
1087
+ */
1088
+ async findGlobal() {
1089
+ return this.list({ where: { tenantId: null } });
1090
+ }
1091
+ /**
1092
+ * Find ad variations for a tenant including global (shared) variations
1093
+ *
1094
+ * @param tenantId - Tenant ID to include
1095
+ * @returns Array of tenant-specific and global ad variations
1096
+ */
1097
+ async findWithGlobals(tenantId2) {
1098
+ return this.query(
1099
+ `SELECT * FROM ad_variations WHERE tenant_id = ? OR tenant_id IS NULL`,
1100
+ [tenantId2]
1101
+ );
1102
+ }
1103
+ }
1104
+ export {
1105
+ AdDeliveryTier,
1106
+ AdDeliveryTierCollection,
1107
+ AdEvent,
1108
+ AdEventCollection,
1109
+ AdEventType,
1110
+ AdFormat,
1111
+ AdFormatCollection,
1112
+ AdFormatType,
1113
+ AdGroup,
1114
+ AdGroupCollection,
1115
+ AdGroupStatus,
1116
+ AdVariation,
1117
+ AdVariationCollection,
1118
+ AdVariationStatus,
1119
+ PricingModel
1120
+ };
1121
+ //# sourceMappingURL=index.js.map