@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/AGENTS.md +47 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +112 -0
- package/dist/index.d.ts +794 -0
- package/dist/index.js +1121 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +3506 -0
- package/dist/smrt-knowledge.json +1438 -0
- package/package.json +63 -0
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
|