@happyvertical/smrt-marketing 0.42.7 → 0.43.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 +14 -1
- package/README.md +45 -0
- package/dist/collections/CampaignCollection.d.ts +29 -2
- package/dist/collections/CampaignCollection.d.ts.map +1 -1
- package/dist/customer-scope.d.ts +14 -0
- package/dist/customer-scope.d.ts.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +362 -11
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +144 -8
- package/dist/models/Campaign.d.ts +5 -0
- package/dist/models/Campaign.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +67 -9
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/models/Campaign.ts","../src/models/CampaignChannel.ts","../src/collections/CampaignChannelCollection.ts","../src/collections/CampaignCollection.ts","../src/models/CampaignMetricSnapshot.ts","../src/collections/CampaignMetricSnapshotCollection.ts","../src/services/BudgetPacingService.ts","../src/services/CampaignLifecycleService.ts","../src/services/MetricIngestionService.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/** Campaign identity, budget, schedule, and guarded lifecycle. */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n CAMPAIGN_STATUSES,\n type CampaignOptions,\n type CampaignStatus,\n} from '../types.js';\n\nconst CAMPAIGN_STATUS_TRANSITIONS: Record<CampaignStatus, CampaignStatus[]> = {\n draft: ['scheduled'],\n scheduled: ['active'],\n active: ['paused', 'completed'],\n paused: ['active', 'completed'],\n completed: ['archived'],\n archived: [],\n};\n\nconst loadedCampaignStatus = new WeakMap<Campaign, CampaignStatus>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaigns',\n conflictColumns: ['tenant_id', 'campaign_key'],\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: { include: ['list', 'get', 'create', 'update'] },\n})\nexport class Campaign extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Stable caller-defined key used by CRM/referral systems. */\n @field({ required: true })\n campaignKey: string = '';\n\n @field({ required: true })\n name: string = '';\n\n /** Open objective key such as `awareness` or `demand_generation`. */\n objective: string = '';\n\n status: CampaignStatus = 'draft';\n\n startAt: Date | null = null;\n\n endAt: Date | null = null;\n\n /** Total campaign budget in integer cents. */\n budgetCents: number = 0;\n\n /** ISO 4217 currency for every campaign/channel amount. */\n currency: string = 'USD';\n\n metadata: string = '{}';\n\n constructor(options: CampaignOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.campaignKey !== undefined)\n this.campaignKey = options.campaignKey;\n if (options.name !== undefined) this.name = options.name;\n if (options.objective !== undefined) this.objective = options.objective;\n if (options.status !== undefined) this.status = options.status;\n if (options.startAt !== undefined)\n this.startAt = Campaign.coerceDate(options.startAt);\n if (options.endAt !== undefined)\n this.endAt = Campaign.coerceDate(options.endAt);\n if (options.budgetCents !== undefined)\n this.budgetCents = options.budgetCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.startAt = Campaign.coerceDate(this.startAt);\n this.endAt = Campaign.coerceDate(this.endAt);\n if (await this.isSaved()) {\n loadedCampaignStatus.set(this, this.status);\n }\n return this;\n }\n\n override async save(): Promise<this> {\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n this.assertSchedule();\n const result = (await super.save()) as this;\n loadedCampaignStatus.set(this, this.status);\n return result;\n }\n\n /** Apply one legal transition in memory; callers choose the save boundary. */\n transitionTo(next: CampaignStatus): this {\n if (next === this.status) return this;\n const allowed = CAMPAIGN_STATUS_TRANSITIONS[this.status] ?? [];\n if (!allowed.includes(next)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: cannot transition ` +\n `'${this.status}' → '${next}'.`,\n );\n }\n this.status = next;\n return this;\n }\n\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n setMetadata(metadata: Record<string, unknown>): void {\n this.metadata = JSON.stringify(metadata ?? {});\n }\n\n private assertSchedule(): void {\n if (\n this.startAt &&\n this.endAt &&\n this.endAt.getTime() < this.startAt.getTime()\n ) {\n throw new Error('Campaign endAt must be on or after startAt.');\n }\n if (!Number.isInteger(this.budgetCents) || this.budgetCents < 0) {\n throw new Error('Campaign budgetCents must be a non-negative integer.');\n }\n }\n\n private async resolvePriorStatus(): Promise<CampaignStatus | undefined> {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) return row.status as CampaignStatus;\n } catch {\n // DB not ready / table absent; fall through to the hydrated state.\n }\n }\n if (this.campaignKey) {\n try {\n const row = await this.db.get(this.tableName, {\n tenant_id: this.tenantId,\n campaign_key: this.campaignKey,\n });\n if (row && row.status != null) return row.status as CampaignStatus;\n } catch {\n // DB not ready / table absent; fall through to the hydrated state.\n }\n }\n return loadedCampaignStatus.get(this);\n }\n\n private assertStatusTransition(prior: CampaignStatus | undefined): void {\n if (!CAMPAIGN_STATUSES.includes(this.status)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: unknown status '${this.status}'.`,\n );\n }\n if (prior === undefined || prior === this.status) return;\n const allowed = CAMPAIGN_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: illegal status transition ` +\n `'${prior}' → '${this.status}'.`,\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default Campaign;\n","/** Generic links from a campaign to cross-package channel execution rows. */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CampaignChannelOptions,\n CampaignChannelStatus,\n} from '../types.js';\nimport { CAMPAIGN_CHANNEL_STATUSES } from '../types.js';\nimport { Campaign } from './Campaign.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaign_channels',\n conflictColumns: ['campaign_id', 'channel_kind', 'channel_ref'],\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: { include: ['list', 'get', 'create', 'update', 'delete'] },\n})\nexport class CampaignChannel extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey(Campaign, { required: true })\n campaignId: string = '';\n\n /** Open kind vocabulary; recommended values live in CAMPAIGN_CHANNEL_KINDS. */\n @field({ required: true })\n channelKind: string = '';\n\n /** Opaque identifier in the namespace named by channelKind. */\n @field({ required: true })\n channelRef: string = '';\n\n allocatedBudgetCents: number = 0;\n\n /** Optional execution-specific override of the Campaign start. */\n startAt: Date | null = null;\n\n /** Optional execution-specific override of the Campaign end. */\n endAt: Date | null = null;\n\n status: CampaignChannelStatus = 'planned';\n\n constructor(options: CampaignChannelOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.campaignId !== undefined) this.campaignId = options.campaignId;\n if (options.channelKind !== undefined)\n this.channelKind = options.channelKind;\n if (options.channelRef !== undefined) this.channelRef = options.channelRef;\n if (options.allocatedBudgetCents !== undefined)\n this.allocatedBudgetCents = options.allocatedBudgetCents;\n if (options.startAt !== undefined)\n this.startAt = CampaignChannel.coerceDate(options.startAt);\n if (options.endAt !== undefined)\n this.endAt = CampaignChannel.coerceDate(options.endAt);\n if (options.status !== undefined) this.status = options.status;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.startAt = CampaignChannel.coerceDate(this.startAt);\n this.endAt = CampaignChannel.coerceDate(this.endAt);\n return this;\n }\n\n override async save(): Promise<this> {\n if (!CAMPAIGN_CHANNEL_STATUSES.includes(this.status)) {\n throw new Error(\n `CampaignChannel ${this.channelRef || this.id}: unknown status '${this.status}'.`,\n );\n }\n if (\n !Number.isInteger(this.allocatedBudgetCents) ||\n this.allocatedBudgetCents < 0\n ) {\n throw new Error(\n 'CampaignChannel allocatedBudgetCents must be a non-negative integer.',\n );\n }\n if (\n this.startAt &&\n this.endAt &&\n this.endAt.getTime() < this.startAt.getTime()\n ) {\n throw new Error('CampaignChannel endAt must be on or after startAt.');\n }\n return (await super.save()) as this;\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default CampaignChannel;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CampaignChannel } from '../models/CampaignChannel.js';\n\nexport class CampaignChannelCollection extends SmrtCollection<CampaignChannel> {\n static readonly _itemClass = CampaignChannel;\n\n async findByCampaign(campaignId: string): Promise<CampaignChannel[]> {\n return await this.list({\n where: { campaignId },\n orderBy: 'start_at ASC',\n });\n }\n\n async findByExecutionRef(\n channelKind: string,\n channelRef: string,\n ): Promise<CampaignChannel[]> {\n return await this.list({ where: { channelKind, channelRef } });\n }\n}\n\nexport default CampaignChannelCollection;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Campaign } from '../models/Campaign.js';\nimport type { CampaignStatus } from '../types.js';\n\nexport class CampaignCollection extends SmrtCollection<Campaign> {\n static readonly _itemClass = Campaign;\n\n async findByCampaignKey(\n campaignKey: string,\n tenantId?: string | null,\n ): Promise<Campaign | null> {\n if (!campaignKey) return null;\n const where: Record<string, unknown> = { campaignKey };\n if (tenantId !== undefined) where.tenantId = tenantId;\n const rows = await this.list({ where, limit: 1 });\n return rows[0] ?? null;\n }\n\n async findByStatus(status: CampaignStatus): Promise<Campaign[]> {\n return await this.list({\n where: { status },\n orderBy: 'start_at ASC',\n });\n }\n}\n\nexport default CampaignCollection;\n","/** Immutable period performance evidence for a campaign or campaign channel. */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\nimport { Campaign } from './Campaign.js';\nimport { CampaignChannel } from './CampaignChannel.js';\n\nconst persistedSnapshotState = new WeakMap<CampaignMetricSnapshot, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaign_metric_snapshots',\n conflictColumns: ['dedupe_key'],\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: { include: ['list', 'get', 'create'] },\n})\nexport class CampaignMetricSnapshot extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey(Campaign, { required: true })\n campaignId: string = '';\n\n /** Null for campaign rollups; set for channel-scoped evidence. */\n @foreignKey(CampaignChannel, { nullable: true })\n campaignChannelId: string | null = null;\n\n @field({ required: true })\n periodStart: Date = new Date();\n\n @field({ required: true })\n periodEnd: Date = new Date();\n\n spendCents: number = 0;\n\n impressions: number = 0;\n\n clicks: number = 0;\n\n conversions: number = 0;\n\n leads: number = 0;\n\n @field({ type: 'integer', nullable: true })\n revenueCents: number | null = null;\n\n /** Ingestion source such as `google_ads`, `meta`, or `rollup_job`. */\n @field({ required: true })\n source: string = '';\n\n @field({ required: true })\n dedupeKey: string = '';\n\n constructor(options: CampaignMetricSnapshotOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.campaignId !== undefined) this.campaignId = options.campaignId;\n if (options.campaignChannelId !== undefined)\n this.campaignChannelId = options.campaignChannelId;\n if (options.periodStart !== undefined)\n this.periodStart =\n CampaignMetricSnapshot.coerceDate(options.periodStart) ??\n new Date(Number.NaN);\n if (options.periodEnd !== undefined)\n this.periodEnd =\n CampaignMetricSnapshot.coerceDate(options.periodEnd) ??\n new Date(Number.NaN);\n if (options.spendCents !== undefined) this.spendCents = options.spendCents;\n if (options.impressions !== undefined)\n this.impressions = options.impressions;\n if (options.clicks !== undefined) this.clicks = options.clicks;\n if (options.conversions !== undefined)\n this.conversions = options.conversions;\n if (options.leads !== undefined) this.leads = options.leads;\n if (options.revenueCents !== undefined)\n this.revenueCents = options.revenueCents;\n if (options.source !== undefined) this.source = options.source;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.periodStart =\n CampaignMetricSnapshot.coerceDate(this.periodStart) ??\n new Date(Number.NaN);\n this.periodEnd =\n CampaignMetricSnapshot.coerceDate(this.periodEnd) ?? new Date(Number.NaN);\n if (this.isPersisted) {\n persistedSnapshotState.set(this, this.serializeState());\n }\n return this;\n }\n\n override async save(): Promise<this> {\n this.assertMetrics();\n await this.assertChannelMatchesCampaign();\n const captured = persistedSnapshotState.get(this);\n if (captured !== undefined) {\n if (captured !== this.serializeState()) {\n throw new Error(\n `CampaignMetricSnapshot ${this.id ?? '<new>'}: metric snapshots ` +\n 'are immutable evidence; ingest a correcting snapshot instead.',\n );\n }\n } else if (this.id && (await this.isSaved())) {\n throw new Error(\n `CampaignMetricSnapshot ${this.id}: refusing to overwrite an existing ` +\n 'snapshot from a non-hydrated instance; snapshots are immutable evidence.',\n );\n } else if (this.dedupeKey) {\n try {\n const row = await this.db.get(this.tableName, {\n dedupe_key: this.dedupeKey,\n });\n if (row && row.id !== this.id) {\n throw new Error(\n `CampaignMetricSnapshot (dedupeKey '${this.dedupeKey}'): a ` +\n 'snapshot with this dedupe key already exists; use ' +\n 'MetricIngestionService for idempotent ingestion.',\n );\n }\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes('idempotent ingestion')\n ) {\n throw error;\n }\n // DB not ready / table absent; there is no persisted row to protect.\n }\n }\n\n const result = (await super.save()) as this;\n persistedSnapshotState.set(this, this.serializeState());\n return result;\n }\n\n override async delete(): Promise<void> {\n throw new Error(\n `CampaignMetricSnapshot ${this.id ?? '<new>'}: metric snapshots are ` +\n 'immutable evidence and cannot be deleted.',\n );\n }\n\n private assertMetrics(): void {\n if (\n Number.isNaN(this.periodStart.getTime()) ||\n Number.isNaN(this.periodEnd.getTime())\n ) {\n throw new Error(\n 'CampaignMetricSnapshot periodStart and periodEnd must be valid dates.',\n );\n }\n if (this.periodEnd.getTime() < this.periodStart.getTime()) {\n throw new Error(\n 'CampaignMetricSnapshot periodEnd must be on or after periodStart.',\n );\n }\n const integerMetrics: Array<[string, number]> = [\n ['spendCents', this.spendCents],\n ['impressions', this.impressions],\n ['clicks', this.clicks],\n ['conversions', this.conversions],\n ['leads', this.leads],\n ];\n for (const [name, value] of integerMetrics) {\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(`${name} must be a non-negative integer.`);\n }\n }\n if (\n this.revenueCents !== null &&\n (!Number.isInteger(this.revenueCents) || this.revenueCents < 0)\n ) {\n throw new Error('revenueCents must be null or a non-negative integer.');\n }\n }\n\n private async assertChannelMatchesCampaign(): Promise<void> {\n if (!this.campaignChannelId) return;\n const channel = (await this.loadRelated(\n 'campaignChannelId',\n )) as CampaignChannel;\n if (channel.campaignId !== this.campaignId) {\n throw new Error(\n `CampaignMetricSnapshot campaignChannelId '${this.campaignChannelId}' ` +\n `does not belong to campaignId '${this.campaignId}'.`,\n );\n }\n }\n\n private serializeState(): string {\n return JSON.stringify({\n tenantId: this.tenantId,\n campaignId: this.campaignId,\n campaignChannelId: this.campaignChannelId,\n periodStart: this.periodStart.toISOString(),\n periodEnd: this.periodEnd.toISOString(),\n spendCents: this.spendCents,\n impressions: this.impressions,\n clicks: this.clicks,\n conversions: this.conversions,\n leads: this.leads,\n revenueCents: this.revenueCents,\n source: this.source,\n dedupeKey: this.dedupeKey,\n });\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default CampaignMetricSnapshot;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\n\nexport class CampaignMetricSnapshotCollection extends SmrtCollection<CampaignMetricSnapshot> {\n static readonly _itemClass = CampaignMetricSnapshot;\n\n async findByDedupeKey(\n dedupeKey: string,\n ): Promise<CampaignMetricSnapshot | null> {\n if (!dedupeKey) return null;\n const rows = await this.list({ where: { dedupeKey }, limit: 1 });\n return rows[0] ?? null;\n }\n\n async getOrCreateByDedupeKey(\n options: CampaignMetricSnapshotOptions,\n ): Promise<{ snapshot: CampaignMetricSnapshot; created: boolean }> {\n const dedupeKey = options.dedupeKey ?? '';\n if (!dedupeKey) {\n throw new Error(\n 'CampaignMetricSnapshotCollection.getOrCreateByDedupeKey requires a dedupeKey.',\n );\n }\n const existing = await this.findByDedupeKey(dedupeKey);\n if (existing) return { snapshot: existing, created: false };\n\n try {\n const snapshot = await this.create({\n ...options,\n // Keep runtime values uncoerced so the model rejects null/invalid\n // periods instead of allowing Date(null) to become the Unix epoch.\n periodStart: options.periodStart as Date | undefined,\n periodEnd: options.periodEnd as Date | undefined,\n _insertOnly: true,\n });\n return { snapshot, created: true };\n } catch (error) {\n // A concurrent ingest may have won the unique dedupe-key insert after\n // our initial read. Re-read the immutable winner instead of allowing a\n // natural-key upsert to replace its evidence in place.\n const concurrent = await this.findByDedupeKey(dedupeKey);\n if (concurrent) return { snapshot: concurrent, created: false };\n throw error;\n }\n }\n\n async findByCampaign(campaignId: string): Promise<CampaignMetricSnapshot[]> {\n return await this.list({\n where: { campaignId },\n orderBy: 'period_start ASC',\n });\n }\n\n async findByChannel(\n campaignChannelId: string,\n ): Promise<CampaignMetricSnapshot[]> {\n return await this.list({\n where: { campaignChannelId },\n orderBy: 'period_start ASC',\n });\n }\n}\n\nexport default CampaignMetricSnapshotCollection;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n CampaignChannelCollection,\n CampaignCollection,\n CampaignMetricSnapshotCollection,\n} from '../collections/index.js';\nimport type { Campaign } from '../models/Campaign.js';\nimport type { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { BudgetPacingResult, BudgetPacingStatus } from '../types.js';\n\n/** Computes spend-to-budget pacing from immutable snapshots; stores nothing. */\nexport class BudgetPacingService {\n constructor(\n private readonly campaigns: CampaignCollection,\n private readonly channels: CampaignChannelCollection,\n private readonly snapshots: CampaignMetricSnapshotCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<BudgetPacingService> {\n return new BudgetPacingService(\n await CampaignCollection.create(classOptions),\n await CampaignChannelCollection.create(classOptions),\n await CampaignMetricSnapshotCollection.create(classOptions),\n );\n }\n\n async getCampaignPacing(\n campaignId: string,\n at = new Date(),\n ): Promise<BudgetPacingResult> {\n const campaign = await this.campaigns.get({ id: campaignId });\n if (!campaign) throw new Error(`Campaign '${campaignId}' was not found.`);\n\n const all = await this.snapshots.findByCampaign(campaignId);\n const rollups = all.filter((snapshot) => !snapshot.campaignChannelId);\n const used = this.selectCampaignEvidence(all);\n return this.calculate({\n campaign,\n snapshots: used,\n budgetCents: campaign.budgetCents,\n startAt: campaign.startAt,\n endAt: campaign.endAt,\n at,\n usedCampaignRollups: rollups.length > 0,\n });\n }\n\n async getChannelPacing(\n campaignChannelId: string,\n at = new Date(),\n ): Promise<BudgetPacingResult> {\n const channel = await this.channels.get({ id: campaignChannelId });\n if (!channel) {\n throw new Error(`CampaignChannel '${campaignChannelId}' was not found.`);\n }\n const campaign = await this.campaigns.get({ id: channel.campaignId });\n if (!campaign) {\n throw new Error(`Campaign '${channel.campaignId}' was not found.`);\n }\n const snapshots = await this.snapshots.findByChannel(campaignChannelId);\n return {\n ...this.calculate({\n campaign,\n snapshots,\n budgetCents: channel.allocatedBudgetCents,\n startAt: channel.startAt ?? campaign.startAt,\n endAt: channel.endAt ?? campaign.endAt,\n at,\n usedCampaignRollups: false,\n }),\n campaignChannelId,\n };\n }\n\n private calculate({\n campaign,\n snapshots,\n budgetCents,\n startAt,\n endAt,\n at,\n usedCampaignRollups,\n }: {\n campaign: Campaign;\n snapshots: CampaignMetricSnapshot[];\n budgetCents: number;\n startAt: Date | null;\n endAt: Date | null;\n at: Date;\n usedCampaignRollups: boolean;\n }): BudgetPacingResult {\n const spendCents = snapshots.reduce(\n (sum, snapshot) => sum + snapshot.spendCents,\n 0,\n );\n const elapsedFraction = this.elapsedFraction(startAt, endAt, at);\n const expectedSpendCents =\n elapsedFraction === null\n ? null\n : Math.round(budgetCents * elapsedFraction);\n const varianceCents =\n expectedSpendCents === null ? null : spendCents - expectedSpendCents;\n const budgetFraction = budgetCents > 0 ? spendCents / budgetCents : null;\n\n return {\n campaignId: campaign.id ?? '',\n currency: campaign.currency,\n budgetCents,\n spendCents,\n remainingCents: budgetCents - spendCents,\n expectedSpendCents,\n varianceCents,\n budgetFraction,\n elapsedFraction,\n status: this.pacingStatus({\n campaign,\n budgetCents,\n spendCents,\n elapsedFraction,\n varianceCents,\n startAt,\n at,\n }),\n snapshotCount: snapshots.length,\n usedCampaignRollups,\n };\n }\n\n private pacingStatus({\n campaign,\n budgetCents,\n spendCents,\n elapsedFraction,\n varianceCents,\n startAt,\n at,\n }: {\n campaign: Campaign;\n budgetCents: number;\n spendCents: number;\n elapsedFraction: number | null;\n varianceCents: number | null;\n startAt: Date | null;\n at: Date;\n }): BudgetPacingStatus {\n if (budgetCents <= 0) return 'unbudgeted';\n if (spendCents > budgetCents) return 'over_budget';\n if (startAt && at.getTime() < startAt.getTime()) {\n return 'not_started';\n }\n if (\n campaign.status === 'completed' ||\n campaign.status === 'archived' ||\n elapsedFraction === 1\n ) {\n return 'complete';\n }\n if (varianceCents === null) return 'on_track';\n\n // A five-percent budget band avoids noisy status flips from minor timing\n // differences between spend collection and scheduled pacing.\n const toleranceCents = Math.max(1, Math.round(budgetCents * 0.05));\n if (varianceCents > toleranceCents) return 'ahead';\n if (varianceCents < -toleranceCents) return 'behind';\n return 'on_track';\n }\n\n /**\n * Prefer a campaign rollup for each exact reporting period, while retaining\n * channel evidence for periods that do not yet have a rollup. This prevents\n * double counting without dropping earlier/later channel-only periods.\n */\n private selectCampaignEvidence(\n snapshots: CampaignMetricSnapshot[],\n ): CampaignMetricSnapshot[] {\n const rollupPeriods = new Set(\n snapshots\n .filter((snapshot) => !snapshot.campaignChannelId)\n .map((snapshot) => this.periodKey(snapshot)),\n );\n return snapshots.filter(\n (snapshot) =>\n !snapshot.campaignChannelId ||\n !rollupPeriods.has(this.periodKey(snapshot)),\n );\n }\n\n private periodKey(snapshot: CampaignMetricSnapshot): string {\n return `${snapshot.periodStart.toISOString()}:${snapshot.periodEnd.toISOString()}`;\n }\n\n private elapsedFraction(\n startAt: Date | null,\n endAt: Date | null,\n at: Date,\n ): number | null {\n if (!startAt || !endAt) return null;\n const start = startAt.getTime();\n const end = endAt.getTime();\n if (end <= start) return at.getTime() < start ? 0 : 1;\n return Math.min(1, Math.max(0, (at.getTime() - start) / (end - start)));\n }\n}\n\nexport default BudgetPacingService;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CampaignCollection } from '../collections/CampaignCollection.js';\nimport type { Campaign } from '../models/Campaign.js';\nimport type { CampaignStatus } from '../types.js';\n\n/** Single authoritative write path for Campaign lifecycle changes. */\nexport class CampaignLifecycleService {\n constructor(private readonly campaigns: CampaignCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CampaignLifecycleService> {\n return new CampaignLifecycleService(\n await CampaignCollection.create(classOptions),\n );\n }\n\n async transition(\n campaignId: string,\n next: CampaignStatus,\n ): Promise<Campaign> {\n const campaign = await this.campaigns.get({ id: campaignId });\n if (!campaign) throw new Error(`Campaign '${campaignId}' was not found.`);\n campaign.transitionTo(next);\n await campaign.save();\n return campaign;\n }\n\n async schedule(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'scheduled');\n }\n\n async activate(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'active');\n }\n\n async pause(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'paused');\n }\n\n async resume(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'active');\n }\n\n async complete(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'completed');\n }\n\n async archive(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'archived');\n }\n}\n\nexport default CampaignLifecycleService;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CampaignMetricSnapshotCollection } from '../collections/CampaignMetricSnapshotCollection.js';\nimport type { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\n\n/** Idempotent, validation-first ingestion for immutable metric evidence. */\nexport class MetricIngestionService {\n constructor(private readonly snapshots: CampaignMetricSnapshotCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<MetricIngestionService> {\n return new MetricIngestionService(\n await CampaignMetricSnapshotCollection.create(classOptions),\n );\n }\n\n async ingest(\n options: CampaignMetricSnapshotOptions,\n ): Promise<{ snapshot: CampaignMetricSnapshot; created: boolean }> {\n if (!options.campaignId) {\n throw new Error('Metric ingestion requires campaignId.');\n }\n if (!options.source?.trim()) {\n throw new Error('Metric ingestion requires source.');\n }\n if (!options.dedupeKey?.trim()) {\n throw new Error('Metric ingestion requires dedupeKey.');\n }\n if (options.periodStart == null || options.periodEnd == null) {\n throw new Error('Metric ingestion requires periodStart and periodEnd.');\n }\n return await this.snapshots.getOrCreateByDedupeKey(options);\n }\n}\n\nexport default MetricIngestionService;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["tenantContext"],"sources":["../src/__smrt-register__.ts","../src/errors.ts","../src/customer-scope.ts","../src/models/Campaign.ts","../src/models/CampaignChannel.ts","../src/collections/CampaignChannelCollection.ts","../src/collections/CampaignCollection.ts","../src/models/CampaignMetricSnapshot.ts","../src/collections/CampaignMetricSnapshotCollection.ts","../src/services/BudgetPacingService.ts","../src/services/CampaignLifecycleService.ts","../src/services/MetricIngestionService.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/** Fail-closed customer/tenant mismatch without revealing row existence. */\nexport class CampaignCustomerScopeError extends Error {\n readonly code = 'CAMPAIGN_CUSTOMER_SCOPE';\n\n constructor(label: string) {\n super(`${label}: customer scope validation failed.`);\n this.name = 'CampaignCustomerScopeError';\n }\n}\n","import {\n ObjectRegistry,\n type SmrtObject,\n type SmrtObjectOptions,\n} from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CampaignCustomerScopeError } from './errors.js';\n\nconst CUSTOMER_QUALIFIED_NAME = '@happyvertical/smrt-commerce:Customer';\nconst SAFE_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/u;\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;\n\ntype QueryDatabase = Pick<DatabaseInterface, 'query' | 'url'>;\n\ninterface DatabaseOptions {\n db?: SmrtObjectOptions['db'];\n}\n\nfunction requireDatabase(options: DatabaseOptions): QueryDatabase {\n const db = options.db;\n if (!db || typeof db !== 'object' || !('query' in db)) {\n throw new Error(\n 'Campaign customer queries require an initialized database connection.',\n );\n }\n return db as QueryDatabase;\n}\n\nasync function resolveCustomerTable(options: DatabaseOptions): Promise<string> {\n await ObjectRegistry.ensureManifestLoaded(CUSTOMER_QUALIFIED_NAME);\n const target =\n ObjectRegistry.getClassByQualifiedName(CUSTOMER_QUALIFIED_NAME) ??\n ObjectRegistry.getClass(CUSTOMER_QUALIFIED_NAME);\n if (!target) {\n throw new Error(\n 'Campaign customer queries require @happyvertical/smrt-commerce:Customer to be registered.',\n );\n }\n\n const probe = new target.constructor(options) as SmrtObject;\n const tableName = probe.tableName;\n if (!SAFE_SQL_IDENTIFIER.test(tableName)) {\n throw new Error('Campaign customer table has an invalid identifier.');\n }\n return tableName;\n}\n\n/**\n * Verify a bounded set of canonical commerce Customers in one database read.\n * The generic failure deliberately does not reveal whether an id is missing or\n * belongs to another tenant.\n */\nexport async function assertCustomersBelongToTenant(\n options: DatabaseOptions,\n tenantId: string | null,\n customerIds: readonly string[],\n label: string,\n lock: 'none' | 'share' | 'update' = 'none',\n): Promise<void> {\n if (customerIds.length === 0) return;\n const normalizedCustomerIds = customerIds.map((customerId) => {\n try {\n return normalizeUuid(customerId, 'customerId');\n } catch {\n throw new CampaignCustomerScopeError(label);\n }\n });\n const normalizedTenantId = tenantId?.toLowerCase() ?? null;\n const db = requireDatabase(options);\n const tableName = await resolveCustomerTable(options);\n const placeholders = normalizedCustomerIds.map(() => '?').join(', ');\n const lockClause = postgresLockClause(db, lock);\n const result = await db.query(\n `SELECT id, tenant_id FROM ${tableName} WHERE id IN (${placeholders})${lockClause}`,\n ...normalizedCustomerIds,\n );\n const tenantById = new Map(\n result.rows.map((row) => [\n String(row.id).toLowerCase(),\n row.tenant_id == null ? null : String(row.tenant_id).toLowerCase(),\n ]),\n );\n const valid = normalizedCustomerIds.every(\n (customerId) =>\n tenantById.has(customerId) &&\n tenantById.get(customerId) === normalizedTenantId,\n );\n if (!valid) {\n throw new CampaignCustomerScopeError(label);\n }\n}\n\nfunction postgresLockClause(\n db: QueryDatabase,\n lock: 'none' | 'share' | 'update',\n): string {\n if (lock === 'none' || !/^postgres(?:ql)?:/iu.test(db.url)) return '';\n return lock === 'update' ? ' FOR UPDATE' : ' FOR SHARE';\n}\n\n/** Parse and canonicalize one UUID without echoing the rejected value. */\nexport function normalizeUuid(value: string, label: string): string {\n if (!UUID_RE.test(value)) {\n throw new Error(`${label} must be a canonical UUID.`);\n }\n return value.toLowerCase();\n}\n","/** Campaign identity, budget, schedule, and guarded lifecycle. */\n\nimport {\n crossPackageRef,\n field,\n SmrtObject,\n smrt,\n ValidationError,\n} from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n isSuperAdminBypass,\n isSystemContext,\n TenantIsolationError,\n TenantScoped,\n tenantId,\n withTenant,\n} from '@happyvertical/smrt-tenancy';\nimport {\n assertCustomersBelongToTenant,\n normalizeUuid,\n} from '../customer-scope.js';\nimport { CampaignCustomerScopeError } from '../errors.js';\nimport {\n CAMPAIGN_STATUSES,\n type CampaignOptions,\n type CampaignStatus,\n} from '../types.js';\n\nconst CAMPAIGN_STATUS_TRANSITIONS: Record<CampaignStatus, CampaignStatus[]> = {\n draft: ['scheduled'],\n scheduled: ['active'],\n active: ['paused', 'completed'],\n paused: ['active', 'completed'],\n completed: ['archived'],\n archived: [],\n};\n\nconst loadedCampaignStatus = new WeakMap<Campaign, CampaignStatus>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaigns',\n conflictColumns: ['tenant_id', 'campaign_key'],\n indexes: [\n {\n name: 'campaigns_tenant_id_customer_id_start_at_id_idx',\n columns: ['tenantId', 'customerId', 'startAt', 'id'],\n },\n ],\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: { include: ['list', 'get', 'create', 'update'] },\n})\nexport class Campaign extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Canonical commerce Customer that owns this campaign, when assigned. */\n @crossPackageRef('@happyvertical/smrt-commerce:Customer', {\n nullable: true,\n validate: true,\n })\n customerId: string | null = null;\n\n /** Stable caller-defined key used by CRM/referral systems. */\n @field({ required: true })\n campaignKey: string = '';\n\n @field({ required: true })\n name: string = '';\n\n /** Open objective key such as `awareness` or `demand_generation`. */\n objective: string = '';\n\n status: CampaignStatus = 'draft';\n\n startAt: Date | null = null;\n\n endAt: Date | null = null;\n\n /** Total campaign budget in integer cents. */\n budgetCents: number = 0;\n\n /** ISO 4217 currency for every campaign/channel amount. */\n currency: string = 'USD';\n\n metadata: string = '{}';\n\n constructor(options: CampaignOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.customerId !== undefined) this.customerId = options.customerId;\n if (options.campaignKey !== undefined)\n this.campaignKey = options.campaignKey;\n if (options.name !== undefined) this.name = options.name;\n if (options.objective !== undefined) this.objective = options.objective;\n if (options.status !== undefined) this.status = options.status;\n if (options.startAt !== undefined)\n this.startAt = Campaign.coerceDate(options.startAt);\n if (options.endAt !== undefined)\n this.endAt = Campaign.coerceDate(options.endAt);\n if (options.budgetCents !== undefined)\n this.budgetCents = options.budgetCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n override async initialize(): Promise<this> {\n if (this.id) {\n try {\n this.id = normalizeUuid(this.id, 'id');\n } catch (error) {\n if (this.customerId !== null) {\n throw new CampaignCustomerScopeError('Campaign.initialize');\n }\n throw error;\n }\n }\n await super.initialize();\n this.startAt = Campaign.coerceDate(this.startAt);\n this.endAt = Campaign.coerceDate(this.endAt);\n if (this.isPersisted) loadedCampaignStatus.set(this, this.status);\n return this;\n }\n\n override async save(): Promise<this> {\n if (this.customerId === null) return this.saveLifecycle();\n\n const db = this.db;\n if (typeof db.transaction !== 'function') {\n throw new Error(\n 'Campaign.save requires a database adapter with transaction support.',\n );\n }\n return db.transaction(async (transactionDb) =>\n this.withDatabase(transactionDb, async (bound) => bound.saveLifecycle()),\n );\n }\n\n private async saveLifecycle(): Promise<this> {\n this.prepareIdentityForSave();\n if (this.customerId !== null) {\n await assertCustomersBelongToTenant(\n this.options,\n this.tenantId,\n [this.customerId],\n 'Campaign.save',\n 'update',\n );\n }\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n this.assertSchedule();\n let result: this;\n try {\n const persist = async () => (await super.save()) as this;\n const tenantContext =\n isSystemContext() || isSuperAdminBypass()\n ? undefined\n : getCurrentTenant();\n result = tenantContext\n ? await withTenant(\n {\n ...tenantContext,\n tenantId: this.tenantId ?? tenantContext.tenantId,\n },\n persist,\n )\n : await persist();\n } catch (error) {\n if (\n error instanceof ValidationError &&\n (error.code === 'VALIDATION_CROSS_PACKAGE_REF_MISSING' ||\n error.code === 'VALIDATION_CROSS_PACKAGE_REF_UNREGISTERED')\n ) {\n throw new CampaignCustomerScopeError('Campaign.save');\n }\n throw error;\n }\n loadedCampaignStatus.set(this, this.status);\n return result;\n }\n\n /** Canonicalize and authorize identity before any status or scope read. */\n private prepareIdentityForSave(): void {\n try {\n if (this.id) this.id = normalizeUuid(this.id, 'id');\n if (this.customerId !== null) {\n this.customerId = normalizeUuid(this.customerId, 'customerId');\n }\n if (this.tenantId !== null) {\n this.tenantId = normalizeUuid(this.tenantId, 'tenantId');\n }\n } catch (error) {\n if (this.customerId !== null) {\n throw new CampaignCustomerScopeError('Campaign.save');\n }\n throw error;\n }\n\n if (isSystemContext() || isSuperAdminBypass()) return;\n const tenantContext = getCurrentTenant();\n if (!tenantContext) return;\n\n let contextTenantId: string;\n try {\n contextTenantId = normalizeUuid(tenantContext.tenantId, 'tenantId');\n } catch (error) {\n if (this.customerId !== null) {\n throw new CampaignCustomerScopeError('Campaign.save');\n }\n throw error;\n }\n if (this.tenantId !== null && this.tenantId !== contextTenantId) {\n throw new TenantIsolationError(\n 'Tenant isolation violation in Campaign.save.',\n { tenantId: contextTenantId, attemptedTenantId: this.tenantId },\n );\n }\n this.tenantId = contextTenantId;\n }\n\n /** Apply one legal transition in memory; callers choose the save boundary. */\n transitionTo(next: CampaignStatus): this {\n if (next === this.status) return this;\n const allowed = CAMPAIGN_STATUS_TRANSITIONS[this.status] ?? [];\n if (!allowed.includes(next)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: cannot transition ` +\n `'${this.status}' → '${next}'.`,\n );\n }\n this.status = next;\n return this;\n }\n\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n setMetadata(metadata: Record<string, unknown>): void {\n this.metadata = JSON.stringify(metadata ?? {});\n }\n\n private assertSchedule(): void {\n if (\n this.startAt &&\n this.endAt &&\n this.endAt.getTime() < this.startAt.getTime()\n ) {\n throw new Error('Campaign endAt must be on or after startAt.');\n }\n if (!Number.isInteger(this.budgetCents) || this.budgetCents < 0) {\n throw new Error('Campaign budgetCents must be a non-negative integer.');\n }\n }\n\n private async resolvePriorStatus(): Promise<CampaignStatus | undefined> {\n if (this.id) {\n let scopedRow: Record<string, unknown> | null = null;\n try {\n scopedRow = await this.db.get(this.tableName, {\n id: this.id,\n tenant_id: this.tenantId,\n });\n } catch {\n // DB not ready / table absent; fall through to the hydrated state.\n }\n if (scopedRow?.status != null) {\n return scopedRow.status as CampaignStatus;\n }\n\n // A caller-supplied ID already owned by another lane must not expose its\n // lifecycle state or be repurposed by an upsert.\n let rowInAnotherLane: Record<string, unknown> | null = null;\n try {\n rowInAnotherLane = await this.db.get(this.tableName, { id: this.id });\n } catch {\n // DB not ready / table absent; fall through to the hydrated state.\n }\n if (rowInAnotherLane) {\n throw new CampaignCustomerScopeError('Campaign.save');\n }\n }\n if (this.campaignKey) {\n try {\n const row = await this.db.get(this.tableName, {\n tenant_id: this.tenantId,\n campaign_key: this.campaignKey,\n });\n if (row && row.status != null) return row.status as CampaignStatus;\n } catch {\n // DB not ready / table absent; fall through to the hydrated state.\n }\n }\n return loadedCampaignStatus.get(this);\n }\n\n private assertStatusTransition(prior: CampaignStatus | undefined): void {\n if (!CAMPAIGN_STATUSES.includes(this.status)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: unknown status '${this.status}'.`,\n );\n }\n if (prior === undefined || prior === this.status) return;\n const allowed = CAMPAIGN_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `Campaign ${this.campaignKey || this.id}: illegal status transition ` +\n `'${prior}' → '${this.status}'.`,\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default Campaign;\n","/** Generic links from a campaign to cross-package channel execution rows. */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CampaignChannelOptions,\n CampaignChannelStatus,\n} from '../types.js';\nimport { CAMPAIGN_CHANNEL_STATUSES } from '../types.js';\nimport { Campaign } from './Campaign.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaign_channels',\n conflictColumns: ['campaign_id', 'channel_kind', 'channel_ref'],\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: { include: ['list', 'get', 'create', 'update', 'delete'] },\n})\nexport class CampaignChannel extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey(Campaign, { required: true })\n campaignId: string = '';\n\n /** Open kind vocabulary; recommended values live in CAMPAIGN_CHANNEL_KINDS. */\n @field({ required: true })\n channelKind: string = '';\n\n /** Opaque identifier in the namespace named by channelKind. */\n @field({ required: true })\n channelRef: string = '';\n\n allocatedBudgetCents: number = 0;\n\n /** Optional execution-specific override of the Campaign start. */\n startAt: Date | null = null;\n\n /** Optional execution-specific override of the Campaign end. */\n endAt: Date | null = null;\n\n status: CampaignChannelStatus = 'planned';\n\n constructor(options: CampaignChannelOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.campaignId !== undefined) this.campaignId = options.campaignId;\n if (options.channelKind !== undefined)\n this.channelKind = options.channelKind;\n if (options.channelRef !== undefined) this.channelRef = options.channelRef;\n if (options.allocatedBudgetCents !== undefined)\n this.allocatedBudgetCents = options.allocatedBudgetCents;\n if (options.startAt !== undefined)\n this.startAt = CampaignChannel.coerceDate(options.startAt);\n if (options.endAt !== undefined)\n this.endAt = CampaignChannel.coerceDate(options.endAt);\n if (options.status !== undefined) this.status = options.status;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.startAt = CampaignChannel.coerceDate(this.startAt);\n this.endAt = CampaignChannel.coerceDate(this.endAt);\n return this;\n }\n\n override async save(): Promise<this> {\n if (!CAMPAIGN_CHANNEL_STATUSES.includes(this.status)) {\n throw new Error(\n `CampaignChannel ${this.channelRef || this.id}: unknown status '${this.status}'.`,\n );\n }\n if (\n !Number.isInteger(this.allocatedBudgetCents) ||\n this.allocatedBudgetCents < 0\n ) {\n throw new Error(\n 'CampaignChannel allocatedBudgetCents must be a non-negative integer.',\n );\n }\n if (\n this.startAt &&\n this.endAt &&\n this.endAt.getTime() < this.startAt.getTime()\n ) {\n throw new Error('CampaignChannel endAt must be on or after startAt.');\n }\n return (await super.save()) as this;\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default CampaignChannel;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CampaignChannel } from '../models/CampaignChannel.js';\n\nexport class CampaignChannelCollection extends SmrtCollection<CampaignChannel> {\n static readonly _itemClass = CampaignChannel;\n\n async findByCampaign(campaignId: string): Promise<CampaignChannel[]> {\n return await this.list({\n where: { campaignId },\n orderBy: 'start_at ASC',\n });\n }\n\n async findByExecutionRef(\n channelKind: string,\n channelRef: string,\n ): Promise<CampaignChannel[]> {\n return await this.list({ where: { channelKind, channelRef } });\n }\n}\n\nexport default CampaignChannelCollection;\n","import {\n type CollectionCacheConfig,\n resolveListLimit,\n SmrtCollection,\n type SmrtListOptions,\n type SmrtSelectedRow,\n type SmrtSelectField,\n type SmrtWhereClause,\n} from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n isSuperAdminBypass,\n isSystemContext,\n TenantIsolationError,\n withTenant,\n} from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertCustomersBelongToTenant,\n normalizeUuid,\n} from '../customer-scope.js';\nimport { Campaign } from '../models/Campaign.js';\nimport type {\n CampaignCustomerCursor,\n CampaignCustomerCursorInput,\n CampaignCustomerPage,\n CampaignCustomerSummary,\n CampaignStatus,\n ListCampaignsByCustomerOptions,\n} from '../types.js';\n\nexport const MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE = 100;\nexport const MAX_CAMPAIGN_CUSTOMER_BATCH_SIZE = 100;\n\nexport class CampaignCollection extends SmrtCollection<Campaign> {\n static readonly _itemClass = Campaign;\n\n override async get(\n filter: string | SmrtWhereClause<Campaign>,\n options: { cache?: CollectionCacheConfig | false } = {},\n ): Promise<Campaign | null> {\n return withCanonicalActiveTenant(() =>\n super.get(normalizeCampaignGetFilter(filter), options),\n );\n }\n\n override async list<\n const Select extends readonly SmrtSelectField<Campaign>[],\n >(\n options: SmrtListOptions<Campaign> & { select: Select; include?: never },\n ): Promise<SmrtSelectedRow<Campaign, Select>[]>;\n override async list(\n options?: Omit<SmrtListOptions<Campaign>, 'select'> & {\n select?: undefined;\n },\n ): Promise<Campaign[]>;\n override async list(\n options: SmrtListOptions<Campaign> = {},\n ): Promise<Campaign[] | Record<string, unknown>[]> {\n return withCanonicalActiveTenant(\n () =>\n super.list(normalizeCampaignListOptions(options) as never) as Promise<\n Campaign[] | Record<string, unknown>[]\n >,\n );\n }\n\n async findByCampaignKey(\n campaignKey: string,\n tenantId?: string | null,\n ): Promise<Campaign | null> {\n if (!campaignKey) return null;\n const where: Record<string, unknown> = { campaignKey };\n if (tenantId !== undefined) where.tenantId = tenantId;\n const rows = await this.list({ where, limit: 1 });\n return rows[0] ?? null;\n }\n\n async findByStatus(status: CampaignStatus): Promise<Campaign[]> {\n return await this.list({\n where: { status },\n orderBy: 'start_at ASC',\n });\n }\n\n /**\n * List one tenant/customer lane newest-first using a stable UUID tiebreaker.\n * Null start times follow all scheduled rows on every supported database.\n */\n async listByCustomer(\n tenantId: string | null,\n customerId: string,\n options: ListCampaignsByCustomerOptions = {},\n ): Promise<CampaignCustomerPage> {\n const normalizedTenantId = normalizeTenantScope(\n tenantId,\n 'CampaignCollection.listByCustomer',\n );\n const normalizedCustomerId = normalizeUuid(customerId, 'customerId');\n const limit = resolveListLimit(options.limit, {\n defaultValue: 50,\n maxValue: Number.MAX_SAFE_INTEGER,\n parameterName: 'campaign customer page limit',\n });\n if (limit > MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE) {\n throw new Error(\n `Campaign customer page limit must not exceed ${MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE}.`,\n );\n }\n if (limit < 1) {\n throw new Error('Campaign customer page limit must be at least 1.');\n }\n const cursor = normalizeCursor(options.after);\n return withCanonicalTenantContext(normalizedTenantId, () =>\n this.inCustomerReadTransaction(async (bound) =>\n bound.listByCustomerInTransaction(\n normalizedTenantId,\n normalizedCustomerId,\n limit,\n cursor,\n ),\n ),\n );\n }\n\n private async listByCustomerInTransaction(\n tenantId: string | null,\n customerId: string,\n limit: number,\n cursor: CampaignCustomerCursor | null,\n ): Promise<CampaignCustomerPage> {\n await assertCustomersBelongToTenant(\n this.options,\n tenantId,\n [customerId],\n 'CampaignCollection.listByCustomer',\n 'share',\n );\n const items =\n cursor?.startAt === null\n ? await this.listNullStartLane(tenantId, customerId, limit + 1, cursor)\n : await this.listScheduledLane(tenantId, customerId, limit, cursor);\n const hasMore = items.length > limit;\n const pageItems = hasMore ? items.slice(0, limit) : items;\n return {\n items: pageItems,\n nextCursor: hasMore\n ? cursorFromCampaign(pageItems[pageItems.length - 1])\n : null,\n };\n }\n\n /**\n * Summarize a bounded customer batch with one scope check and one grouped\n * aggregate query. Every requested customer receives a row, including zeros.\n */\n async summarizeByCustomers(\n tenantId: string | null,\n customerIds: string[],\n ): Promise<CampaignCustomerSummary[]> {\n const normalizedTenantId = normalizeTenantScope(\n tenantId,\n 'CampaignCollection.summarizeByCustomers',\n );\n if (!Array.isArray(customerIds)) {\n throw new Error(\n 'Campaign customer summary customerIds must be an array.',\n );\n }\n if (customerIds.length > MAX_CAMPAIGN_CUSTOMER_BATCH_SIZE) {\n throw new Error(\n `Campaign customer summary accepts at most ${MAX_CAMPAIGN_CUSTOMER_BATCH_SIZE} customerIds.`,\n );\n }\n const normalizedCustomerIds = customerIds.map((customerId) =>\n normalizeUuid(customerId, 'customerId'),\n );\n const uniqueIds = [...new Set(normalizedCustomerIds)];\n if (uniqueIds.length === 0) return [];\n\n return withCanonicalTenantContext(normalizedTenantId, () =>\n this.inCustomerReadTransaction(async (bound) =>\n bound.summarizeByCustomersInTransaction(\n normalizedTenantId,\n normalizedCustomerIds,\n uniqueIds,\n ),\n ),\n );\n }\n\n private async summarizeByCustomersInTransaction(\n tenantId: string | null,\n requestedCustomerIds: string[],\n uniqueIds: string[],\n ): Promise<CampaignCustomerSummary[]> {\n await assertCustomersBelongToTenant(\n this.options,\n tenantId,\n uniqueIds,\n 'CampaignCollection.summarizeByCustomers',\n 'share',\n );\n const db = requireDatabase(this.options);\n const placeholders = uniqueIds.map(() => '?').join(', ');\n const tenantPredicate =\n tenantId === null ? 'tenant_id IS NULL' : 'tenant_id = ?';\n const tenantParams = tenantId === null ? [] : [tenantId];\n const result = await db.query(\n `SELECT customer_id,\n COUNT(*) AS total_count,\n SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS active_count,\n MAX(start_at) AS latest_start_at\n FROM ${this.tableName}\n WHERE ${tenantPredicate} AND customer_id IN (${placeholders})\n GROUP BY customer_id\n ORDER BY customer_id ASC\n LIMIT ?`,\n 'active',\n ...tenantParams,\n ...uniqueIds,\n uniqueIds.length,\n );\n const byCustomer = new Map(\n result.rows.map((row) => [String(row.customer_id).toLowerCase(), row]),\n );\n return requestedCustomerIds.map((customerId) => {\n const row = byCustomer.get(customerId);\n return {\n customerId,\n totalCount: row\n ? toSafeCount(row.total_count, 'Campaign customer total count')\n : 0,\n activeCount: row\n ? toSafeCount(row.active_count, 'Campaign customer active count')\n : 0,\n latestStartAt: row ? coerceDate(row.latest_start_at) : null,\n };\n });\n }\n\n private async inCustomerReadTransaction<T>(\n operation: (bound: CampaignCollection) => Promise<T>,\n ): Promise<T> {\n const db = this.db as DatabaseInterface;\n if (typeof db.transaction !== 'function') {\n throw new Error(\n 'Campaign customer reads require a database adapter with transaction support.',\n );\n }\n return db.transaction(async (transactionDb) => {\n const bound = await CampaignCollection.create({\n ...this.options,\n db: transactionDb,\n defaultListLimit: undefined,\n maxListLimit: undefined,\n });\n return operation(bound);\n });\n }\n\n private async listScheduledLane(\n tenantId: string | null,\n customerId: string,\n limit: number,\n cursor: CampaignCustomerCursor | null,\n ): Promise<Campaign[]> {\n const base = [{ tenantId }, { customerId }, { 'startAt !=': null }];\n const where = cursor\n ? [\n [...base, { 'startAt <': cursor.startAt }],\n [...base, { startAt: cursor.startAt }, { 'id <': cursor.id }],\n ]\n : { tenantId, customerId, 'startAt !=': null };\n const scheduled = await this.list({\n where,\n orderBy: ['start_at DESC', 'id DESC'],\n limit: limit + 1,\n });\n if (scheduled.length > limit) return scheduled;\n const nullRows = await this.listNullStartLane(\n tenantId,\n customerId,\n limit - scheduled.length + 1,\n null,\n );\n return [...scheduled, ...nullRows];\n }\n\n private async listNullStartLane(\n tenantId: string | null,\n customerId: string,\n limit: number,\n cursor: CampaignCustomerCursor | null,\n ): Promise<Campaign[]> {\n return this.list({\n where: {\n tenantId,\n customerId,\n startAt: null,\n ...(cursor ? { 'id <': cursor.id } : {}),\n },\n orderBy: 'id DESC',\n limit,\n });\n }\n}\n\ninterface QueryDatabase {\n query(\n sql: string,\n ...params: unknown[]\n ): Promise<{ rows: Array<Record<string, unknown>> }>;\n}\n\nfunction requireDatabase(options: { db?: unknown }): QueryDatabase {\n const db = options.db;\n if (!db || typeof db !== 'object' || !('query' in db)) {\n throw new Error(\n 'Campaign customer summaries require an initialized database connection.',\n );\n }\n return db as QueryDatabase;\n}\n\nfunction coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n const date =\n value instanceof Date\n ? value\n : new Date(typeof value === 'number' ? value : String(value));\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\nfunction toSafeCount(value: unknown, label: string): number {\n const count = Number(value);\n if (!Number.isSafeInteger(count) || count < 0) {\n throw new Error(`${label} is outside the safe integer range.`);\n }\n return count;\n}\n\nfunction normalizeCursor(\n cursor: CampaignCustomerCursorInput | undefined,\n): CampaignCustomerCursor | null {\n if (!cursor) return null;\n const id = normalizeUuid(cursor.id, 'campaign customer cursor id');\n if (cursor.startAt === null) return { id, startAt: null };\n const startAt = coerceDate(cursor.startAt);\n if (!startAt) throw new Error('campaign customer cursor startAt is invalid.');\n return { id, startAt };\n}\n\nfunction cursorFromCampaign(\n campaign: Campaign | undefined,\n): CampaignCustomerCursor | null {\n if (!campaign?.id) return null;\n return { id: campaign.id, startAt: campaign.startAt };\n}\n\nfunction normalizeTenantScope(\n tenantId: string | null,\n label: string,\n): string | null {\n if (tenantId !== null) {\n const normalized = normalizeUuid(tenantId, 'tenantId');\n const tenantContext = getCurrentTenant();\n if (tenantContext && !isSuperAdminBypass()) {\n const contextTenantId = normalizeUuid(tenantContext.tenantId, 'tenantId');\n if (normalized !== contextTenantId) {\n throw new TenantIsolationError(\n `Tenant isolation violation in ${label}.`,\n { tenantId: contextTenantId, attemptedTenantId: normalized },\n );\n }\n }\n return normalized;\n }\n const tenantContext = getCurrentTenant();\n if (tenantContext && !isSuperAdminBypass() && !isSystemContext()) {\n throw new TenantIsolationError(\n `Tenant isolation violation in ${label}: tenant context cannot read global Campaign rows.`,\n { tenantId: tenantContext.tenantId, attemptedTenantId: 'global' },\n );\n }\n return null;\n}\n\nfunction withCanonicalTenantContext<T>(\n tenantId: string | null,\n operation: () => Promise<T>,\n): Promise<T> {\n const tenantContext = getCurrentTenant();\n return tenantId !== null &&\n tenantContext &&\n !isSuperAdminBypass() &&\n !isSystemContext()\n ? withTenant({ ...tenantContext, tenantId }, operation)\n : operation();\n}\n\nfunction withCanonicalActiveTenant<T>(operation: () => Promise<T>): Promise<T> {\n const tenantContext = getCurrentTenant();\n if (!tenantContext || isSuperAdminBypass() || isSystemContext()) {\n return operation();\n }\n const normalizedTenantId = normalizeUuid(tenantContext.tenantId, 'tenantId');\n return normalizedTenantId === tenantContext.tenantId\n ? operation()\n : withTenant({ ...tenantContext, tenantId: normalizedTenantId }, operation);\n}\n\nfunction normalizeCampaignGetFilter(\n filter: string | SmrtWhereClause<Campaign>,\n): string | SmrtWhereClause<Campaign> {\n if (typeof filter !== 'string') return normalizeCampaignUuidWhere(filter);\n try {\n return normalizeUuid(filter, 'campaign id');\n } catch {\n return filter;\n }\n}\n\nfunction normalizeCampaignListOptions(\n options: SmrtListOptions<Campaign>,\n): SmrtListOptions<Campaign> {\n return options.where === undefined\n ? options\n : { ...options, where: normalizeCampaignUuidWhere(options.where) };\n}\n\nfunction normalizeCampaignUuidWhere<T>(where: T): T {\n if (Array.isArray(where)) {\n return where.map((entry) => normalizeCampaignUuidWhere(entry)) as T;\n }\n if (!where || typeof where !== 'object') return where;\n\n const normalized: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(where)) {\n const field = key.trim().split(/\\s+/u)[0];\n const label =\n field === 'id'\n ? 'campaign id'\n : field === 'customerId'\n ? 'customerId'\n : field === 'tenantId'\n ? 'tenantId'\n : null;\n if (!label) {\n normalized[key] = value;\n } else if (Array.isArray(value)) {\n normalized[key] = value.map((uuid) =>\n typeof uuid === 'string' ? normalizeUuid(uuid, label) : uuid,\n );\n } else {\n normalized[key] =\n typeof value === 'string' ? normalizeUuid(value, label) : value;\n }\n }\n return normalized as T;\n}\n\nexport default CampaignCollection;\n","/** Immutable period performance evidence for a campaign or campaign channel. */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\nimport { Campaign } from './Campaign.js';\nimport { CampaignChannel } from './CampaignChannel.js';\n\nconst persistedSnapshotState = new WeakMap<CampaignMetricSnapshot, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'campaign_metric_snapshots',\n conflictColumns: ['dedupe_key'],\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: { include: ['list', 'get', 'create'] },\n})\nexport class CampaignMetricSnapshot extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey(Campaign, { required: true })\n campaignId: string = '';\n\n /** Null for campaign rollups; set for channel-scoped evidence. */\n @foreignKey(CampaignChannel, { nullable: true })\n campaignChannelId: string | null = null;\n\n @field({ required: true })\n periodStart: Date = new Date();\n\n @field({ required: true })\n periodEnd: Date = new Date();\n\n spendCents: number = 0;\n\n impressions: number = 0;\n\n clicks: number = 0;\n\n conversions: number = 0;\n\n leads: number = 0;\n\n @field({ type: 'integer', nullable: true })\n revenueCents: number | null = null;\n\n /** Ingestion source such as `google_ads`, `meta`, or `rollup_job`. */\n @field({ required: true })\n source: string = '';\n\n @field({ required: true })\n dedupeKey: string = '';\n\n constructor(options: CampaignMetricSnapshotOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.campaignId !== undefined) this.campaignId = options.campaignId;\n if (options.campaignChannelId !== undefined)\n this.campaignChannelId = options.campaignChannelId;\n if (options.periodStart !== undefined)\n this.periodStart =\n CampaignMetricSnapshot.coerceDate(options.periodStart) ??\n new Date(Number.NaN);\n if (options.periodEnd !== undefined)\n this.periodEnd =\n CampaignMetricSnapshot.coerceDate(options.periodEnd) ??\n new Date(Number.NaN);\n if (options.spendCents !== undefined) this.spendCents = options.spendCents;\n if (options.impressions !== undefined)\n this.impressions = options.impressions;\n if (options.clicks !== undefined) this.clicks = options.clicks;\n if (options.conversions !== undefined)\n this.conversions = options.conversions;\n if (options.leads !== undefined) this.leads = options.leads;\n if (options.revenueCents !== undefined)\n this.revenueCents = options.revenueCents;\n if (options.source !== undefined) this.source = options.source;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.periodStart =\n CampaignMetricSnapshot.coerceDate(this.periodStart) ??\n new Date(Number.NaN);\n this.periodEnd =\n CampaignMetricSnapshot.coerceDate(this.periodEnd) ?? new Date(Number.NaN);\n if (this.isPersisted) {\n persistedSnapshotState.set(this, this.serializeState());\n }\n return this;\n }\n\n override async save(): Promise<this> {\n this.assertMetrics();\n await this.assertChannelMatchesCampaign();\n const captured = persistedSnapshotState.get(this);\n if (captured !== undefined) {\n if (captured !== this.serializeState()) {\n throw new Error(\n `CampaignMetricSnapshot ${this.id ?? '<new>'}: metric snapshots ` +\n 'are immutable evidence; ingest a correcting snapshot instead.',\n );\n }\n } else if (this.id && (await this.isSaved())) {\n throw new Error(\n `CampaignMetricSnapshot ${this.id}: refusing to overwrite an existing ` +\n 'snapshot from a non-hydrated instance; snapshots are immutable evidence.',\n );\n } else if (this.dedupeKey) {\n try {\n const row = await this.db.get(this.tableName, {\n dedupe_key: this.dedupeKey,\n });\n if (row && row.id !== this.id) {\n throw new Error(\n `CampaignMetricSnapshot (dedupeKey '${this.dedupeKey}'): a ` +\n 'snapshot with this dedupe key already exists; use ' +\n 'MetricIngestionService for idempotent ingestion.',\n );\n }\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes('idempotent ingestion')\n ) {\n throw error;\n }\n // DB not ready / table absent; there is no persisted row to protect.\n }\n }\n\n const result = (await super.save()) as this;\n persistedSnapshotState.set(this, this.serializeState());\n return result;\n }\n\n override async delete(): Promise<void> {\n throw new Error(\n `CampaignMetricSnapshot ${this.id ?? '<new>'}: metric snapshots are ` +\n 'immutable evidence and cannot be deleted.',\n );\n }\n\n private assertMetrics(): void {\n if (\n Number.isNaN(this.periodStart.getTime()) ||\n Number.isNaN(this.periodEnd.getTime())\n ) {\n throw new Error(\n 'CampaignMetricSnapshot periodStart and periodEnd must be valid dates.',\n );\n }\n if (this.periodEnd.getTime() < this.periodStart.getTime()) {\n throw new Error(\n 'CampaignMetricSnapshot periodEnd must be on or after periodStart.',\n );\n }\n const integerMetrics: Array<[string, number]> = [\n ['spendCents', this.spendCents],\n ['impressions', this.impressions],\n ['clicks', this.clicks],\n ['conversions', this.conversions],\n ['leads', this.leads],\n ];\n for (const [name, value] of integerMetrics) {\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(`${name} must be a non-negative integer.`);\n }\n }\n if (\n this.revenueCents !== null &&\n (!Number.isInteger(this.revenueCents) || this.revenueCents < 0)\n ) {\n throw new Error('revenueCents must be null or a non-negative integer.');\n }\n }\n\n private async assertChannelMatchesCampaign(): Promise<void> {\n if (!this.campaignChannelId) return;\n const channel = (await this.loadRelated(\n 'campaignChannelId',\n )) as CampaignChannel;\n if (channel.campaignId !== this.campaignId) {\n throw new Error(\n `CampaignMetricSnapshot campaignChannelId '${this.campaignChannelId}' ` +\n `does not belong to campaignId '${this.campaignId}'.`,\n );\n }\n }\n\n private serializeState(): string {\n return JSON.stringify({\n tenantId: this.tenantId,\n campaignId: this.campaignId,\n campaignChannelId: this.campaignChannelId,\n periodStart: this.periodStart.toISOString(),\n periodEnd: this.periodEnd.toISOString(),\n spendCents: this.spendCents,\n impressions: this.impressions,\n clicks: this.clicks,\n conversions: this.conversions,\n leads: this.leads,\n revenueCents: this.revenueCents,\n source: this.source,\n dedupeKey: this.dedupeKey,\n });\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n return null;\n }\n}\n\nexport default CampaignMetricSnapshot;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\n\nexport class CampaignMetricSnapshotCollection extends SmrtCollection<CampaignMetricSnapshot> {\n static readonly _itemClass = CampaignMetricSnapshot;\n\n async findByDedupeKey(\n dedupeKey: string,\n ): Promise<CampaignMetricSnapshot | null> {\n if (!dedupeKey) return null;\n const rows = await this.list({ where: { dedupeKey }, limit: 1 });\n return rows[0] ?? null;\n }\n\n async getOrCreateByDedupeKey(\n options: CampaignMetricSnapshotOptions,\n ): Promise<{ snapshot: CampaignMetricSnapshot; created: boolean }> {\n const dedupeKey = options.dedupeKey ?? '';\n if (!dedupeKey) {\n throw new Error(\n 'CampaignMetricSnapshotCollection.getOrCreateByDedupeKey requires a dedupeKey.',\n );\n }\n const existing = await this.findByDedupeKey(dedupeKey);\n if (existing) return { snapshot: existing, created: false };\n\n try {\n const snapshot = await this.create({\n ...options,\n // Keep runtime values uncoerced so the model rejects null/invalid\n // periods instead of allowing Date(null) to become the Unix epoch.\n periodStart: options.periodStart as Date | undefined,\n periodEnd: options.periodEnd as Date | undefined,\n _insertOnly: true,\n });\n return { snapshot, created: true };\n } catch (error) {\n // A concurrent ingest may have won the unique dedupe-key insert after\n // our initial read. Re-read the immutable winner instead of allowing a\n // natural-key upsert to replace its evidence in place.\n const concurrent = await this.findByDedupeKey(dedupeKey);\n if (concurrent) return { snapshot: concurrent, created: false };\n throw error;\n }\n }\n\n async findByCampaign(campaignId: string): Promise<CampaignMetricSnapshot[]> {\n return await this.list({\n where: { campaignId },\n orderBy: 'period_start ASC',\n });\n }\n\n async findByChannel(\n campaignChannelId: string,\n ): Promise<CampaignMetricSnapshot[]> {\n return await this.list({\n where: { campaignChannelId },\n orderBy: 'period_start ASC',\n });\n }\n}\n\nexport default CampaignMetricSnapshotCollection;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n CampaignChannelCollection,\n CampaignCollection,\n CampaignMetricSnapshotCollection,\n} from '../collections/index.js';\nimport type { Campaign } from '../models/Campaign.js';\nimport type { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { BudgetPacingResult, BudgetPacingStatus } from '../types.js';\n\n/** Computes spend-to-budget pacing from immutable snapshots; stores nothing. */\nexport class BudgetPacingService {\n constructor(\n private readonly campaigns: CampaignCollection,\n private readonly channels: CampaignChannelCollection,\n private readonly snapshots: CampaignMetricSnapshotCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<BudgetPacingService> {\n return new BudgetPacingService(\n await CampaignCollection.create(classOptions),\n await CampaignChannelCollection.create(classOptions),\n await CampaignMetricSnapshotCollection.create(classOptions),\n );\n }\n\n async getCampaignPacing(\n campaignId: string,\n at = new Date(),\n ): Promise<BudgetPacingResult> {\n const campaign = await this.campaigns.get({ id: campaignId });\n if (!campaign) throw new Error(`Campaign '${campaignId}' was not found.`);\n\n const all = await this.snapshots.findByCampaign(campaignId);\n const rollups = all.filter((snapshot) => !snapshot.campaignChannelId);\n const used = this.selectCampaignEvidence(all);\n return this.calculate({\n campaign,\n snapshots: used,\n budgetCents: campaign.budgetCents,\n startAt: campaign.startAt,\n endAt: campaign.endAt,\n at,\n usedCampaignRollups: rollups.length > 0,\n });\n }\n\n async getChannelPacing(\n campaignChannelId: string,\n at = new Date(),\n ): Promise<BudgetPacingResult> {\n const channel = await this.channels.get({ id: campaignChannelId });\n if (!channel) {\n throw new Error(`CampaignChannel '${campaignChannelId}' was not found.`);\n }\n const campaign = await this.campaigns.get({ id: channel.campaignId });\n if (!campaign) {\n throw new Error(`Campaign '${channel.campaignId}' was not found.`);\n }\n const snapshots = await this.snapshots.findByChannel(campaignChannelId);\n return {\n ...this.calculate({\n campaign,\n snapshots,\n budgetCents: channel.allocatedBudgetCents,\n startAt: channel.startAt ?? campaign.startAt,\n endAt: channel.endAt ?? campaign.endAt,\n at,\n usedCampaignRollups: false,\n }),\n campaignChannelId,\n };\n }\n\n private calculate({\n campaign,\n snapshots,\n budgetCents,\n startAt,\n endAt,\n at,\n usedCampaignRollups,\n }: {\n campaign: Campaign;\n snapshots: CampaignMetricSnapshot[];\n budgetCents: number;\n startAt: Date | null;\n endAt: Date | null;\n at: Date;\n usedCampaignRollups: boolean;\n }): BudgetPacingResult {\n const spendCents = snapshots.reduce(\n (sum, snapshot) => sum + snapshot.spendCents,\n 0,\n );\n const elapsedFraction = this.elapsedFraction(startAt, endAt, at);\n const expectedSpendCents =\n elapsedFraction === null\n ? null\n : Math.round(budgetCents * elapsedFraction);\n const varianceCents =\n expectedSpendCents === null ? null : spendCents - expectedSpendCents;\n const budgetFraction = budgetCents > 0 ? spendCents / budgetCents : null;\n\n return {\n campaignId: campaign.id ?? '',\n currency: campaign.currency,\n budgetCents,\n spendCents,\n remainingCents: budgetCents - spendCents,\n expectedSpendCents,\n varianceCents,\n budgetFraction,\n elapsedFraction,\n status: this.pacingStatus({\n campaign,\n budgetCents,\n spendCents,\n elapsedFraction,\n varianceCents,\n startAt,\n at,\n }),\n snapshotCount: snapshots.length,\n usedCampaignRollups,\n };\n }\n\n private pacingStatus({\n campaign,\n budgetCents,\n spendCents,\n elapsedFraction,\n varianceCents,\n startAt,\n at,\n }: {\n campaign: Campaign;\n budgetCents: number;\n spendCents: number;\n elapsedFraction: number | null;\n varianceCents: number | null;\n startAt: Date | null;\n at: Date;\n }): BudgetPacingStatus {\n if (budgetCents <= 0) return 'unbudgeted';\n if (spendCents > budgetCents) return 'over_budget';\n if (startAt && at.getTime() < startAt.getTime()) {\n return 'not_started';\n }\n if (\n campaign.status === 'completed' ||\n campaign.status === 'archived' ||\n elapsedFraction === 1\n ) {\n return 'complete';\n }\n if (varianceCents === null) return 'on_track';\n\n // A five-percent budget band avoids noisy status flips from minor timing\n // differences between spend collection and scheduled pacing.\n const toleranceCents = Math.max(1, Math.round(budgetCents * 0.05));\n if (varianceCents > toleranceCents) return 'ahead';\n if (varianceCents < -toleranceCents) return 'behind';\n return 'on_track';\n }\n\n /**\n * Prefer a campaign rollup for each exact reporting period, while retaining\n * channel evidence for periods that do not yet have a rollup. This prevents\n * double counting without dropping earlier/later channel-only periods.\n */\n private selectCampaignEvidence(\n snapshots: CampaignMetricSnapshot[],\n ): CampaignMetricSnapshot[] {\n const rollupPeriods = new Set(\n snapshots\n .filter((snapshot) => !snapshot.campaignChannelId)\n .map((snapshot) => this.periodKey(snapshot)),\n );\n return snapshots.filter(\n (snapshot) =>\n !snapshot.campaignChannelId ||\n !rollupPeriods.has(this.periodKey(snapshot)),\n );\n }\n\n private periodKey(snapshot: CampaignMetricSnapshot): string {\n return `${snapshot.periodStart.toISOString()}:${snapshot.periodEnd.toISOString()}`;\n }\n\n private elapsedFraction(\n startAt: Date | null,\n endAt: Date | null,\n at: Date,\n ): number | null {\n if (!startAt || !endAt) return null;\n const start = startAt.getTime();\n const end = endAt.getTime();\n if (end <= start) return at.getTime() < start ? 0 : 1;\n return Math.min(1, Math.max(0, (at.getTime() - start) / (end - start)));\n }\n}\n\nexport default BudgetPacingService;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CampaignCollection } from '../collections/CampaignCollection.js';\nimport type { Campaign } from '../models/Campaign.js';\nimport type { CampaignStatus } from '../types.js';\n\n/** Single authoritative write path for Campaign lifecycle changes. */\nexport class CampaignLifecycleService {\n constructor(private readonly campaigns: CampaignCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CampaignLifecycleService> {\n return new CampaignLifecycleService(\n await CampaignCollection.create(classOptions),\n );\n }\n\n async transition(\n campaignId: string,\n next: CampaignStatus,\n ): Promise<Campaign> {\n const campaign = await this.campaigns.get({ id: campaignId });\n if (!campaign) throw new Error(`Campaign '${campaignId}' was not found.`);\n campaign.transitionTo(next);\n await campaign.save();\n return campaign;\n }\n\n async schedule(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'scheduled');\n }\n\n async activate(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'active');\n }\n\n async pause(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'paused');\n }\n\n async resume(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'active');\n }\n\n async complete(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'completed');\n }\n\n async archive(campaignId: string): Promise<Campaign> {\n return await this.transition(campaignId, 'archived');\n }\n}\n\nexport default CampaignLifecycleService;\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CampaignMetricSnapshotCollection } from '../collections/CampaignMetricSnapshotCollection.js';\nimport type { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { CampaignMetricSnapshotOptions } from '../types.js';\n\n/** Idempotent, validation-first ingestion for immutable metric evidence. */\nexport class MetricIngestionService {\n constructor(private readonly snapshots: CampaignMetricSnapshotCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<MetricIngestionService> {\n return new MetricIngestionService(\n await CampaignMetricSnapshotCollection.create(classOptions),\n );\n }\n\n async ingest(\n options: CampaignMetricSnapshotOptions,\n ): Promise<{ snapshot: CampaignMetricSnapshot; created: boolean }> {\n if (!options.campaignId) {\n throw new Error('Metric ingestion requires campaignId.');\n }\n if (!options.source?.trim()) {\n throw new Error('Metric ingestion requires source.');\n }\n if (!options.dedupeKey?.trim()) {\n throw new Error('Metric ingestion requires dedupeKey.');\n }\n if (options.periodStart == null || options.periodEnd == null) {\n throw new Error('Metric ingestion requires periodStart and periodEnd.');\n }\n return await this.snapshots.getOrCreateByDedupeKey(options);\n }\n}\n\nexport default MetricIngestionService;\n"],"mappings":"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"}
|
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-marketing",
|
|
5
|
-
"packageVersion": "0.
|
|
5
|
+
"packageVersion": "0.43.0",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-marketing:CampaignChannelCollection": {
|
|
8
8
|
"name": "campaignchannelcollection",
|
|
@@ -112,6 +112,39 @@
|
|
|
112
112
|
"packageName": "@happyvertical/smrt-marketing",
|
|
113
113
|
"fields": {},
|
|
114
114
|
"methods": {
|
|
115
|
+
"get": {
|
|
116
|
+
"name": "get",
|
|
117
|
+
"async": true,
|
|
118
|
+
"parameters": [
|
|
119
|
+
{
|
|
120
|
+
"name": "filter",
|
|
121
|
+
"type": "string | SmrtWhereClause<Campaign>",
|
|
122
|
+
"optional": false
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "options",
|
|
126
|
+
"type": "object",
|
|
127
|
+
"optional": true
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"returnType": "Promise<Campaign | null>",
|
|
131
|
+
"isStatic": false,
|
|
132
|
+
"isPublic": true
|
|
133
|
+
},
|
|
134
|
+
"list": {
|
|
135
|
+
"name": "list",
|
|
136
|
+
"async": true,
|
|
137
|
+
"parameters": [
|
|
138
|
+
{
|
|
139
|
+
"name": "options",
|
|
140
|
+
"type": "SmrtListOptions<Campaign>",
|
|
141
|
+
"optional": true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"returnType": "Promise<Campaign[] | Record<string>[]>",
|
|
145
|
+
"isStatic": false,
|
|
146
|
+
"isPublic": true
|
|
147
|
+
},
|
|
115
148
|
"findByCampaignKey": {
|
|
116
149
|
"name": "findByCampaignKey",
|
|
117
150
|
"async": true,
|
|
@@ -144,6 +177,49 @@
|
|
|
144
177
|
"returnType": "Promise<Campaign[]>",
|
|
145
178
|
"isStatic": false,
|
|
146
179
|
"isPublic": true
|
|
180
|
+
},
|
|
181
|
+
"listByCustomer": {
|
|
182
|
+
"name": "listByCustomer",
|
|
183
|
+
"async": true,
|
|
184
|
+
"parameters": [
|
|
185
|
+
{
|
|
186
|
+
"name": "tenantId",
|
|
187
|
+
"type": "string | null",
|
|
188
|
+
"optional": false
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "customerId",
|
|
192
|
+
"type": "string",
|
|
193
|
+
"optional": false
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "options",
|
|
197
|
+
"type": "ListCampaignsByCustomerOptions",
|
|
198
|
+
"optional": true
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"returnType": "Promise<CampaignCustomerPage>",
|
|
202
|
+
"isStatic": false,
|
|
203
|
+
"isPublic": true
|
|
204
|
+
},
|
|
205
|
+
"summarizeByCustomers": {
|
|
206
|
+
"name": "summarizeByCustomers",
|
|
207
|
+
"async": true,
|
|
208
|
+
"parameters": [
|
|
209
|
+
{
|
|
210
|
+
"name": "tenantId",
|
|
211
|
+
"type": "string | null",
|
|
212
|
+
"optional": false
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"name": "customerIds",
|
|
216
|
+
"type": "string[]",
|
|
217
|
+
"optional": false
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"returnType": "Promise<CampaignCustomerSummary[]>",
|
|
221
|
+
"isStatic": false,
|
|
222
|
+
"isPublic": true
|
|
147
223
|
}
|
|
148
224
|
},
|
|
149
225
|
"decoratorConfig": {
|
|
@@ -350,6 +426,15 @@
|
|
|
350
426
|
}
|
|
351
427
|
}
|
|
352
428
|
},
|
|
429
|
+
"customerId": {
|
|
430
|
+
"type": "crossPackageRef",
|
|
431
|
+
"required": false,
|
|
432
|
+
"related": "@happyvertical/smrt-commerce:Customer",
|
|
433
|
+
"_meta": {
|
|
434
|
+
"validate": true,
|
|
435
|
+
"nullable": true
|
|
436
|
+
}
|
|
437
|
+
},
|
|
353
438
|
"campaignKey": {
|
|
354
439
|
"type": "text",
|
|
355
440
|
"required": true,
|
|
@@ -460,6 +545,17 @@
|
|
|
460
545
|
"tenant_id",
|
|
461
546
|
"campaign_key"
|
|
462
547
|
],
|
|
548
|
+
"indexes": [
|
|
549
|
+
{
|
|
550
|
+
"name": "campaigns_tenant_id_customer_id_start_at_id_idx",
|
|
551
|
+
"columns": [
|
|
552
|
+
"tenantId",
|
|
553
|
+
"customerId",
|
|
554
|
+
"startAt",
|
|
555
|
+
"id"
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
],
|
|
463
559
|
"api": {
|
|
464
560
|
"include": [
|
|
465
561
|
"list",
|
|
@@ -505,7 +601,7 @@
|
|
|
505
601
|
],
|
|
506
602
|
"schema": {
|
|
507
603
|
"tableName": "campaigns",
|
|
508
|
-
"ddl": "CREATE TABLE IF NOT EXISTS \"campaigns\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"campaign_key\" TEXT NOT NULL DEFAULT '',\n \"name\" TEXT NOT NULL DEFAULT '',\n \"objective\" TEXT DEFAULT '',\n \"status\" TEXT DEFAULT 'draft',\n \"start_at\" TIMESTAMP,\n \"end_at\" TIMESTAMP,\n \"budget_cents\" INTEGER DEFAULT 0,\n \"currency\" TEXT DEFAULT 'USD',\n \"metadata\" TEXT DEFAULT '{}'\n);",
|
|
604
|
+
"ddl": "CREATE TABLE IF NOT EXISTS \"campaigns\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"customer_id\" UUID,\n \"campaign_key\" TEXT NOT NULL DEFAULT '',\n \"name\" TEXT NOT NULL DEFAULT '',\n \"objective\" TEXT DEFAULT '',\n \"status\" TEXT DEFAULT 'draft',\n \"start_at\" TIMESTAMP,\n \"end_at\" TIMESTAMP,\n \"budget_cents\" INTEGER DEFAULT 0,\n \"currency\" TEXT DEFAULT 'USD',\n \"metadata\" TEXT DEFAULT '{}'\n);",
|
|
509
605
|
"columns": {
|
|
510
606
|
"id": {
|
|
511
607
|
"type": "UUID",
|
|
@@ -538,6 +634,12 @@
|
|
|
538
634
|
"notNull": false,
|
|
539
635
|
"unique": false
|
|
540
636
|
},
|
|
637
|
+
"customer_id": {
|
|
638
|
+
"type": "UUID",
|
|
639
|
+
"referenceKind": "crossPackageRef",
|
|
640
|
+
"notNull": false,
|
|
641
|
+
"unique": false
|
|
642
|
+
},
|
|
541
643
|
"campaign_key": {
|
|
542
644
|
"type": "TEXT",
|
|
543
645
|
"notNull": true,
|
|
@@ -607,15 +709,30 @@
|
|
|
607
709
|
"context"
|
|
608
710
|
]
|
|
609
711
|
},
|
|
712
|
+
{
|
|
713
|
+
"name": "campaigns_tenant_id_customer_id_start_at_id_idx",
|
|
714
|
+
"columns": [
|
|
715
|
+
"tenant_id",
|
|
716
|
+
"customer_id",
|
|
717
|
+
"start_at",
|
|
718
|
+
"id"
|
|
719
|
+
]
|
|
720
|
+
},
|
|
610
721
|
{
|
|
611
722
|
"name": "campaigns_tenant_id_created_at_idx",
|
|
612
723
|
"columns": [
|
|
613
724
|
"tenant_id",
|
|
614
725
|
"created_at"
|
|
615
726
|
]
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
"name": "campaigns_customer_id_idx",
|
|
730
|
+
"columns": [
|
|
731
|
+
"customer_id"
|
|
732
|
+
]
|
|
616
733
|
}
|
|
617
734
|
],
|
|
618
|
-
"version": "
|
|
735
|
+
"version": "d0945102"
|
|
619
736
|
}
|
|
620
737
|
},
|
|
621
738
|
"@happyvertical/smrt-marketing:CampaignChannel": {
|
|
@@ -801,7 +918,13 @@
|
|
|
801
918
|
"type": "UUID",
|
|
802
919
|
"referenceKind": "foreignKey",
|
|
803
920
|
"notNull": true,
|
|
804
|
-
"unique": false
|
|
921
|
+
"unique": false,
|
|
922
|
+
"foreignKey": {
|
|
923
|
+
"table": "campaigns",
|
|
924
|
+
"column": "id",
|
|
925
|
+
"onDelete": "CASCADE",
|
|
926
|
+
"onUpdate": "CASCADE"
|
|
927
|
+
}
|
|
805
928
|
},
|
|
806
929
|
"channel_kind": {
|
|
807
930
|
"type": "TEXT",
|
|
@@ -863,7 +986,7 @@
|
|
|
863
986
|
]
|
|
864
987
|
}
|
|
865
988
|
],
|
|
866
|
-
"version": "
|
|
989
|
+
"version": "cc045525"
|
|
867
990
|
}
|
|
868
991
|
},
|
|
869
992
|
"@happyvertical/smrt-marketing:CampaignMetricSnapshot": {
|
|
@@ -1096,13 +1219,25 @@
|
|
|
1096
1219
|
"type": "UUID",
|
|
1097
1220
|
"referenceKind": "foreignKey",
|
|
1098
1221
|
"notNull": true,
|
|
1099
|
-
"unique": false
|
|
1222
|
+
"unique": false,
|
|
1223
|
+
"foreignKey": {
|
|
1224
|
+
"table": "campaigns",
|
|
1225
|
+
"column": "id",
|
|
1226
|
+
"onDelete": "NO ACTION",
|
|
1227
|
+
"onUpdate": "CASCADE"
|
|
1228
|
+
}
|
|
1100
1229
|
},
|
|
1101
1230
|
"campaign_channel_id": {
|
|
1102
1231
|
"type": "UUID",
|
|
1103
1232
|
"referenceKind": "foreignKey",
|
|
1104
1233
|
"notNull": false,
|
|
1105
|
-
"unique": false
|
|
1234
|
+
"unique": false,
|
|
1235
|
+
"foreignKey": {
|
|
1236
|
+
"table": "campaign_channels",
|
|
1237
|
+
"column": "id",
|
|
1238
|
+
"onDelete": "NO ACTION",
|
|
1239
|
+
"onUpdate": "CASCADE"
|
|
1240
|
+
}
|
|
1106
1241
|
},
|
|
1107
1242
|
"period_start": {
|
|
1108
1243
|
"type": "TIMESTAMP",
|
|
@@ -1197,12 +1332,13 @@
|
|
|
1197
1332
|
]
|
|
1198
1333
|
}
|
|
1199
1334
|
],
|
|
1200
|
-
"version": "
|
|
1335
|
+
"version": "1ccd36b3"
|
|
1201
1336
|
}
|
|
1202
1337
|
}
|
|
1203
1338
|
},
|
|
1204
1339
|
"moduleType": "smrt",
|
|
1205
1340
|
"smrtDependencies": [
|
|
1341
|
+
"@happyvertical/smrt-commerce",
|
|
1206
1342
|
"@happyvertical/smrt-core",
|
|
1207
1343
|
"@happyvertical/smrt-tenancy"
|
|
1208
1344
|
]
|
|
@@ -2,6 +2,8 @@ import { SmrtObject } from '@happyvertical/smrt-core';
|
|
|
2
2
|
import { CampaignOptions, CampaignStatus } from '../types.js';
|
|
3
3
|
export declare class Campaign extends SmrtObject {
|
|
4
4
|
tenantId: string | null;
|
|
5
|
+
/** Canonical commerce Customer that owns this campaign, when assigned. */
|
|
6
|
+
customerId: string | null;
|
|
5
7
|
/** Stable caller-defined key used by CRM/referral systems. */
|
|
6
8
|
campaignKey: string;
|
|
7
9
|
name: string;
|
|
@@ -18,6 +20,9 @@ export declare class Campaign extends SmrtObject {
|
|
|
18
20
|
constructor(options?: CampaignOptions);
|
|
19
21
|
initialize(): Promise<this>;
|
|
20
22
|
save(): Promise<this>;
|
|
23
|
+
private saveLifecycle;
|
|
24
|
+
/** Canonicalize and authorize identity before any status or scope read. */
|
|
25
|
+
private prepareIdentityForSave;
|
|
21
26
|
/** Apply one legal transition in memory; callers choose the save boundary. */
|
|
22
27
|
transitionTo(next: CampaignStatus): this;
|
|
23
28
|
getMetadata(): Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Campaign.d.ts","sourceRoot":"","sources":["../../src/models/Campaign.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,
|
|
1
|
+
{"version":3,"file":"Campaign.d.ts","sourceRoot":"","sources":["../../src/models/Campaign.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,EAGL,UAAU,EAGX,MAAM,0BAA0B,CAAC;AAelC,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAarB,qBAca,QAAS,SAAQ,UAAU;IAEtC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,0EAA0E;IAK1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,8DAA8D;IAE9D,WAAW,EAAE,MAAM,CAAM;IAGzB,IAAI,EAAE,MAAM,CAAM;IAElB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAM;IAEvB,MAAM,EAAE,cAAc,CAAW;IAEjC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE5B,KAAK,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE1B,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAK;IAExB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAS;IAEzB,QAAQ,EAAE,MAAM,CAAQ;gBAEZ,OAAO,GAAE,eAAoB;IAmB1B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YActB,aAAa;IA4C3B,2EAA2E;IAC3E,OAAO,CAAC,sBAAsB;IAsC9B,8EAA8E;IAC9E,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAaxC,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAYtC,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIpD,OAAO,CAAC,cAAc;YAaR,kBAAkB;IAyChC,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,MAAM,CAAC,UAAU;CAS1B;AAED,eAAe,QAAQ,CAAC"}
|