@happyvertical/smrt-marketing 0.43.9 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +23 -4
  3. package/dist/collections/CampaignCollection.d.ts +10 -1
  4. package/dist/collections/CampaignCollection.d.ts.map +1 -1
  5. package/dist/index.js +448 -256
  6. package/dist/index.js.map +1 -1
  7. package/dist/manifest.json +25 -1
  8. package/dist/services/BudgetPacingService.d.ts +0 -3
  9. package/dist/services/BudgetPacingService.d.ts.map +1 -1
  10. package/dist/services/pacing-calculation.d.ts +18 -0
  11. package/dist/services/pacing-calculation.d.ts.map +1 -0
  12. package/dist/smrt-knowledge.json +16 -5
  13. package/dist/svelte/components/BudgetPacing.svelte +3 -0
  14. package/dist/svelte/components/BudgetPacing.svelte.d.ts +3 -0
  15. package/dist/svelte/components/BudgetPacing.svelte.d.ts.map +1 -1
  16. package/dist/svelte/components/CampaignDetail.svelte +5 -0
  17. package/dist/svelte/components/CampaignDetail.svelte.d.ts +5 -0
  18. package/dist/svelte/components/CampaignDetail.svelte.d.ts.map +1 -1
  19. package/dist/svelte/components/CampaignList.svelte +4 -0
  20. package/dist/svelte/components/CampaignList.svelte.d.ts +4 -0
  21. package/dist/svelte/components/CampaignList.svelte.d.ts.map +1 -1
  22. package/dist/svelte/components/ChannelMix.svelte +3 -0
  23. package/dist/svelte/components/ChannelMix.svelte.d.ts +3 -0
  24. package/dist/svelte/components/ChannelMix.svelte.d.ts.map +1 -1
  25. package/dist/svelte/components/MarketingDashboard.svelte +4 -0
  26. package/dist/svelte/components/MarketingDashboard.svelte.d.ts +4 -0
  27. package/dist/svelte/components/MarketingDashboard.svelte.d.ts.map +1 -1
  28. package/dist/types.d.ts +28 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/package.json +7 -6
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.js","names":["elapsedFraction","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/services/pacing-calculation.ts","../src/models/CampaignMetricSnapshot.ts","../src/collections/CampaignMetricSnapshotCollection.ts","../src/collections/CampaignCollection.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 type { Campaign } from '../models/Campaign.js';\nimport type { BudgetPacingResult, BudgetPacingStatus } from '../types.js';\n\nexport interface BudgetPacingEvidence {\n spendCents: number;\n snapshotCount: number;\n usedCampaignRollups: boolean;\n}\n\n/** Canonical campaign pacing calculation for grouped and row-level evidence. */\nexport function calculateCampaignPacing(\n campaign: Campaign,\n evidence: BudgetPacingEvidence,\n at = new Date(),\n): BudgetPacingResult {\n return calculatePacing({\n campaign,\n evidence,\n budgetCents: campaign.budgetCents,\n startAt: campaign.startAt,\n endAt: campaign.endAt,\n at,\n });\n}\n\nexport function calculatePacing({\n campaign,\n evidence,\n budgetCents,\n startAt,\n endAt,\n at,\n}: {\n campaign: Campaign;\n evidence: BudgetPacingEvidence;\n budgetCents: number;\n startAt: Date | null;\n endAt: Date | null;\n at: Date;\n}): BudgetPacingResult {\n const elapsed = elapsedFraction(startAt, endAt, at);\n const expectedSpendCents =\n elapsed === null ? null : Math.round(budgetCents * elapsed);\n const varianceCents =\n expectedSpendCents === null\n ? null\n : evidence.spendCents - expectedSpendCents;\n return {\n campaignId: campaign.id ?? '',\n currency: campaign.currency,\n budgetCents,\n spendCents: evidence.spendCents,\n remainingCents: budgetCents - evidence.spendCents,\n expectedSpendCents,\n varianceCents,\n budgetFraction: budgetCents > 0 ? evidence.spendCents / budgetCents : null,\n elapsedFraction: elapsed,\n status: pacingStatus({\n campaign,\n budgetCents,\n spendCents: evidence.spendCents,\n elapsedFraction: elapsed,\n varianceCents,\n startAt,\n at,\n }),\n snapshotCount: evidence.snapshotCount,\n usedCampaignRollups: evidence.usedCampaignRollups,\n };\n}\n\nfunction 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()) return 'not_started';\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\nfunction 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","/** 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 {\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 { calculateCampaignPacing } from '../services/pacing-calculation.js';\nimport type {\n CampaignChannelMixEntry,\n CampaignCustomerCursor,\n CampaignCustomerCursorInput,\n CampaignCustomerPage,\n CampaignCustomerSummary,\n CampaignMetricTotals,\n CampaignReportingPage,\n CampaignStatus,\n ListCampaignReportingByCustomerOptions,\n ListCampaignsByCustomerOptions,\n} from '../types.js';\nimport { CampaignChannelCollection } from './CampaignChannelCollection.js';\nimport { CampaignMetricSnapshotCollection } from './CampaignMetricSnapshotCollection.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 'CampaignCollection.listByCustomer',\n ),\n ),\n );\n }\n\n /**\n * Return one bounded customer page with its complete read-only reporting\n * projection. Channels and immutable evidence are aggregated in two grouped\n * reads, independent of page size; no campaign callback or lazy load runs.\n */\n async listReportingByCustomer(\n tenantId: string | null,\n customerId: string,\n options: ListCampaignReportingByCustomerOptions = {},\n ): Promise<CampaignReportingPage> {\n const normalizedTenantId = normalizeTenantScope(\n tenantId,\n 'CampaignCollection.listReportingByCustomer',\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 reporting page limit',\n });\n if (limit > MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE) {\n throw new Error(\n `Campaign reporting page limit must not exceed ${MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE}.`,\n );\n }\n if (limit < 1) {\n throw new Error('Campaign reporting page limit must be at least 1.');\n }\n const cursor = normalizeCursor(options.after);\n const at = normalizeReportingAt(options.at);\n\n return withCanonicalTenantContext(normalizedTenantId, () =>\n this.inCustomerReadTransaction(async (bound) => {\n const page = await bound.listByCustomerInTransaction(\n normalizedTenantId,\n normalizedCustomerId,\n limit,\n cursor,\n 'CampaignCollection.listReportingByCustomer',\n );\n return bound.projectReportingPage(normalizedTenantId, page, at);\n }),\n );\n }\n\n private async projectReportingPage(\n tenantId: string | null,\n page: CampaignCustomerPage,\n at: Date,\n ): Promise<CampaignReportingPage> {\n const campaignIds = page.items.map((campaign) => {\n if (!campaign.id) {\n throw new Error('Campaign reporting requires persisted Campaign rows.');\n }\n return normalizeUuid(campaign.id, 'campaign id');\n });\n if (campaignIds.length === 0) {\n return { items: [], nextCursor: page.nextCursor };\n }\n\n const channelRows = await this.loadChannelMixRows(tenantId, campaignIds);\n const metricRows = await this.loadMetricRows(tenantId, campaignIds);\n const channelsByCampaign = groupChannelMix(channelRows);\n const metricsByCampaign = groupMetricTotals(metricRows);\n\n return {\n items: page.items.map((campaign) => {\n const campaignId = campaign.id ?? '';\n const channelMix = channelsByCampaign.get(campaignId) ?? [];\n const metrics =\n metricsByCampaign.get(campaignId) ?? emptyMetricTotals();\n return {\n campaign,\n channelCount: channelMix.reduce(\n (sum, entry) =>\n checkedAdd(sum, entry.count, 'Campaign channel count'),\n 0,\n ),\n channelMix,\n metricTotals: metrics.totals,\n pacing: calculateCampaignPacing(\n campaign,\n {\n spendCents: metrics.totals.spendCents,\n snapshotCount: metrics.snapshotCount,\n usedCampaignRollups: metrics.usedCampaignRollups,\n },\n at,\n ),\n };\n }),\n nextCursor: page.nextCursor,\n };\n }\n\n private async loadChannelMixRows(\n tenantId: string | null,\n campaignIds: string[],\n ): Promise<Array<Record<string, unknown>>> {\n const channels = await CampaignChannelCollection.create({\n ...this.options,\n db: this.db,\n defaultListLimit: undefined,\n maxListLimit: undefined,\n });\n const db = requireDatabase(this.options);\n const placeholders = campaignIds.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 campaign_id, channel_kind, COUNT(*) AS channel_count\n FROM ${channels.tableName}\n WHERE ${tenantPredicate} AND campaign_id IN (${placeholders})\n GROUP BY campaign_id, channel_kind\n ORDER BY campaign_id ASC, channel_kind ASC`,\n ...tenantParams,\n ...campaignIds,\n );\n return result.rows;\n }\n\n private async loadMetricRows(\n tenantId: string | null,\n campaignIds: string[],\n ): Promise<Array<Record<string, unknown>>> {\n const snapshots = await CampaignMetricSnapshotCollection.create({\n ...this.options,\n db: this.db,\n defaultListLimit: undefined,\n maxListLimit: undefined,\n });\n const db = requireDatabase(this.options);\n const placeholders = campaignIds.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 `WITH scoped_snapshots AS (\n SELECT campaign_id,\n campaign_channel_id,\n period_start,\n period_end,\n spend_cents,\n impressions,\n clicks,\n conversions,\n leads,\n revenue_cents\n FROM ${snapshots.tableName}\n WHERE ${tenantPredicate} AND campaign_id IN (${placeholders})\n ), rollup_periods AS (\n SELECT DISTINCT campaign_id, period_start, period_end\n FROM scoped_snapshots\n WHERE campaign_channel_id IS NULL\n ), selected_snapshots AS (\n SELECT s.*\n FROM scoped_snapshots s\n LEFT JOIN rollup_periods r\n ON r.campaign_id = s.campaign_id\n AND r.period_start = s.period_start\n AND r.period_end = s.period_end\n WHERE s.campaign_channel_id IS NULL OR r.campaign_id IS NULL\n )\n SELECT campaign_id,\n COUNT(*) AS snapshot_count,\n MAX(CASE WHEN campaign_channel_id IS NULL THEN 1 ELSE 0 END)\n AS used_campaign_rollups,\n SUM(spend_cents) AS spend_cents,\n SUM(impressions) AS impressions,\n SUM(clicks) AS clicks,\n SUM(conversions) AS conversions,\n SUM(leads) AS leads,\n SUM(COALESCE(revenue_cents, 0)) AS revenue_cents\n FROM selected_snapshots\n GROUP BY campaign_id\n ORDER BY campaign_id ASC`,\n ...tenantParams,\n ...campaignIds,\n );\n return result.rows;\n }\n\n private async listByCustomerInTransaction(\n tenantId: string | null,\n customerId: string,\n limit: number,\n cursor: CampaignCustomerCursor | null,\n operation: string,\n ): Promise<CampaignCustomerPage> {\n await assertCustomersBelongToTenant(\n this.options,\n tenantId,\n [customerId],\n operation,\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\ninterface GroupedMetricTotals {\n totals: CampaignMetricTotals;\n snapshotCount: number;\n usedCampaignRollups: boolean;\n}\n\nfunction groupChannelMix(\n rows: Array<Record<string, unknown>>,\n): Map<string, CampaignChannelMixEntry[]> {\n const grouped = new Map<string, CampaignChannelMixEntry[]>();\n for (const row of rows) {\n const campaignId = normalizeUuid(String(row.campaign_id), 'campaign id');\n const channelKind = String(row.channel_kind);\n if (!channelKind) throw new Error('Campaign channel kind is empty.');\n const entry = {\n channelKind,\n count: toSafeCount(row.channel_count, 'Campaign channel count'),\n };\n const entries = grouped.get(campaignId) ?? [];\n entries.push(entry);\n grouped.set(campaignId, entries);\n }\n return grouped;\n}\n\nfunction groupMetricTotals(\n rows: Array<Record<string, unknown>>,\n): Map<string, GroupedMetricTotals> {\n return new Map(\n rows.map((row) => {\n const campaignId = normalizeUuid(String(row.campaign_id), 'campaign id');\n return [\n campaignId,\n {\n totals: {\n spendCents: toSafeCount(row.spend_cents, 'Campaign spend total'),\n impressions: toSafeCount(\n row.impressions,\n 'Campaign impression total',\n ),\n clicks: toSafeCount(row.clicks, 'Campaign click total'),\n conversions: toSafeCount(\n row.conversions,\n 'Campaign conversion total',\n ),\n leads: toSafeCount(row.leads, 'Campaign lead total'),\n revenueCents: toSafeCount(\n row.revenue_cents,\n 'Campaign revenue total',\n ),\n },\n snapshotCount: toSafeCount(\n row.snapshot_count,\n 'Campaign snapshot count',\n ),\n usedCampaignRollups: toBooleanFlag(row.used_campaign_rollups),\n },\n ];\n }),\n );\n}\n\nfunction emptyMetricTotals(): GroupedMetricTotals {\n return {\n totals: {\n spendCents: 0,\n impressions: 0,\n clicks: 0,\n conversions: 0,\n leads: 0,\n revenueCents: 0,\n },\n snapshotCount: 0,\n usedCampaignRollups: false,\n };\n}\n\nfunction toBooleanFlag(value: unknown): boolean {\n return value === true || value === 1 || value === '1';\n}\n\nfunction checkedAdd(left: number, right: number, label: string): number {\n const sum = left + right;\n if (!Number.isSafeInteger(sum) || sum < 0) {\n throw new Error(`${label} is outside the safe integer range.`);\n }\n return sum;\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 reporting and 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 normalizeReportingAt(value: Date | string | number | undefined): Date {\n if (value === undefined) return new Date();\n const at = coerceDate(value);\n if (!at) throw new Error('campaign reporting at is invalid.');\n return at;\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","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n CampaignChannelCollection,\n CampaignCollection,\n CampaignMetricSnapshotCollection,\n} from '../collections/index.js';\nimport type { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';\nimport type { BudgetPacingResult } from '../types.js';\nimport {\n calculateCampaignPacing,\n calculatePacing,\n} from './pacing-calculation.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 calculateCampaignPacing(\n campaign,\n {\n spendCents: used.reduce(\n (sum, snapshot) => sum + snapshot.spendCents,\n 0,\n ),\n snapshotCount: used.length,\n usedCampaignRollups: rollups.length > 0,\n },\n at,\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 ...calculatePacing({\n campaign,\n evidence: {\n spendCents: snapshots.reduce(\n (sum, snapshot) => sum + snapshot.spendCents,\n 0,\n ),\n snapshotCount: snapshots.length,\n usedCampaignRollups: false,\n },\n budgetCents: channel.allocatedBudgetCents,\n startAt: channel.startAt ?? campaign.startAt,\n endAt: channel.endAt ?? campaign.endAt,\n at,\n }),\n campaignChannelId,\n };\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\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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"}
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-marketing",
5
- "packageVersion": "0.43.9",
5
+ "packageVersion": "0.44.0",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-marketing:CampaignChannelCollection": {
8
8
  "name": "campaignchannelcollection",
@@ -202,6 +202,30 @@
202
202
  "isStatic": false,
203
203
  "isPublic": true
204
204
  },
205
+ "listReportingByCustomer": {
206
+ "name": "listReportingByCustomer",
207
+ "async": true,
208
+ "parameters": [
209
+ {
210
+ "name": "tenantId",
211
+ "type": "string | null",
212
+ "optional": false
213
+ },
214
+ {
215
+ "name": "customerId",
216
+ "type": "string",
217
+ "optional": false
218
+ },
219
+ {
220
+ "name": "options",
221
+ "type": "ListCampaignReportingByCustomerOptions",
222
+ "optional": true
223
+ }
224
+ ],
225
+ "returnType": "Promise<CampaignReportingPage>",
226
+ "isStatic": false,
227
+ "isPublic": true
228
+ },
205
229
  "summarizeByCustomers": {
206
230
  "name": "summarizeByCustomers",
207
231
  "async": true,
@@ -10,8 +10,6 @@ export declare class BudgetPacingService {
10
10
  static create(classOptions?: SmrtClassOptions): Promise<BudgetPacingService>;
11
11
  getCampaignPacing(campaignId: string, at?: Date): Promise<BudgetPacingResult>;
12
12
  getChannelPacing(campaignChannelId: string, at?: Date): Promise<BudgetPacingResult>;
13
- private calculate;
14
- private pacingStatus;
15
13
  /**
16
14
  * Prefer a campaign rollup for each exact reporting period, while retaining
17
15
  * channel evidence for periods that do not yet have a rollup. This prevents
@@ -19,7 +17,6 @@ export declare class BudgetPacingService {
19
17
  */
20
18
  private selectCampaignEvidence;
21
19
  private periodKey;
22
- private elapsedFraction;
23
20
  }
24
21
  export default BudgetPacingService;
25
22
  //# sourceMappingURL=BudgetPacingService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BudgetPacingService.d.ts","sourceRoot":"","sources":["../../src/services/BudgetPacingService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACL,yBAAyB,EACzB,kBAAkB,EAClB,gCAAgC,EACjC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,aAAa,CAAC;AAE1E,gFAAgF;AAChF,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,SAAS,EAAE,kBAAkB,EAC7B,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,gCAAgC;WAGjD,MAAM,CACjB,YAAY,GAAE,gBAAqB,GAClC,OAAO,CAAC,mBAAmB,CAAC;IAQzB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,EAAE,OAAa,GACd,OAAO,CAAC,kBAAkB,CAAC;IAkBxB,gBAAgB,CACpB,iBAAiB,EAAE,MAAM,EACzB,EAAE,OAAa,GACd,OAAO,CAAC,kBAAkB,CAAC;IAwB9B,OAAO,CAAC,SAAS;IAsDjB,OAAO,CAAC,YAAY;IAuCpB;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,eAAe;CAWxB;AAED,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"BudgetPacingService.d.ts","sourceRoot":"","sources":["../../src/services/BudgetPacingService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACL,yBAAyB,EACzB,kBAAkB,EAClB,gCAAgC,EACjC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMtD,gFAAgF;AAChF,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,SAAS,EAAE,kBAAkB,EAC7B,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,gCAAgC;WAGjD,MAAM,CACjB,YAAY,GAAE,gBAAqB,GAClC,OAAO,CAAC,mBAAmB,CAAC;IAQzB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,EAAE,OAAa,GACd,OAAO,CAAC,kBAAkB,CAAC;IAqBxB,gBAAgB,CACpB,iBAAiB,EAAE,MAAM,EACzB,EAAE,OAAa,GACd,OAAO,CAAC,kBAAkB,CAAC;IA8B9B;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,SAAS;CAGlB;AAED,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { Campaign } from '../models/Campaign.js';
2
+ import { BudgetPacingResult } from '../types.js';
3
+ export interface BudgetPacingEvidence {
4
+ spendCents: number;
5
+ snapshotCount: number;
6
+ usedCampaignRollups: boolean;
7
+ }
8
+ /** Canonical campaign pacing calculation for grouped and row-level evidence. */
9
+ export declare function calculateCampaignPacing(campaign: Campaign, evidence: BudgetPacingEvidence, at?: Date): BudgetPacingResult;
10
+ export declare function calculatePacing({ campaign, evidence, budgetCents, startAt, endAt, at, }: {
11
+ campaign: Campaign;
12
+ evidence: BudgetPacingEvidence;
13
+ budgetCents: number;
14
+ startAt: Date | null;
15
+ endAt: Date | null;
16
+ at: Date;
17
+ }): BudgetPacingResult;
18
+ //# sourceMappingURL=pacing-calculation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pacing-calculation.d.ts","sourceRoot":"","sources":["../../src/services/pacing-calculation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,aAAa,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,oBAAoB,EAC9B,EAAE,OAAa,GACd,kBAAkB,CASpB;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,EACL,EAAE,GACH,EAAE;IACD,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,EAAE,EAAE,IAAI,CAAC;CACV,GAAG,kBAAkB,CA8BrB"}
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-marketing",
6
- "packageVersion": "0.43.9",
6
+ "packageVersion": "0.44.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "660ca6e940136780ad87ecf5285186d5eec85ca3c32bfb0fd08f34c214cd314b",
11
- "packageJson": "3dcead17f01c0393fcfa3c67864a6b9e735fa061e4c41ea1fc56d6e4c8f70aee",
12
- "agents": "0d11e523b80990f78607047ca887445747292970990881333c60231397703924"
10
+ "manifest": "4a86c63f2ffb751ee1227dfa0f145eee410ff35e65c64812824df3d6d6228d2f",
11
+ "packageJson": "16418aa6cf2ce9239249b68c31d6463ac8de0e5b5a924d6555e3b0a334df5af1",
12
+ "agents": "3b81365608ec4f1929b51db17194f9c1f74d420b49886277e0f94b42238915d8"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -102,6 +102,7 @@
102
102
  "get",
103
103
  "list",
104
104
  "listByCustomer",
105
+ "listReportingByCustomer",
105
106
  "summarizeByCustomers"
106
107
  ],
107
108
  "methodSignatures": [
@@ -149,6 +150,16 @@
149
150
  ],
150
151
  "returns": "Promise<CampaignCustomerPage>"
151
152
  },
153
+ {
154
+ "name": "listReportingByCustomer",
155
+ "async": true,
156
+ "params": [
157
+ "tenantId: string | null",
158
+ "customerId: string",
159
+ "options?: ListCampaignReportingByCustomerOptions"
160
+ ],
161
+ "returns": "Promise<CampaignReportingPage>"
162
+ },
152
163
  {
153
164
  "name": "summarizeByCustomers",
154
165
  "async": true,
@@ -1106,5 +1117,5 @@
1106
1117
  "polymorphicAssociations": 0,
1107
1118
  "uuidColumns": 13
1108
1119
  },
1109
- "agentDoc": "# @happyvertical/smrt-marketing\n\nCross-channel campaign coordination for SMRT. This package owns Campaign\nidentity, budgets, lifecycle, execution links, immutable performance evidence,\ncomputed pacing, and props-driven Svelte surfaces. It does not execute ads,\npublish social posts, send messages, or own sales attribution.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-marketing test` and\n`pnpm --filter @happyvertical/smrt-marketing typecheck`; use `test:coverage`\nto exercise the model/service and component coverage configurations.\nNatural-key changes must also run\n`pnpm --filter @happyvertical/smrt-marketing test:postgres` when\nthe disposable PostgreSQL harness is available. Use\n`pnpm --filter @happyvertical/smrt-marketing build` and `verify:pack` for\npublish-surface changes.\n\n## Models\n\n- **Campaign**: optional-tenant umbrella with stable natural key\n `(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,\n schedule, optional native UUID reference to the canonical commerce Customer,\n guarded metadata helpers, and lifecycle\n `draft → scheduled → active ↔ paused → completed → archived`. Raw saves are\n protected by an authoritative prior-status re-read; use\n `CampaignLifecycleService` for lifecycle writes.\n- **CampaignChannel**: one generic execution link per\n `(campaign_id, channel_kind, channel_ref)`. `channelKind`/`channelRef` are\n intentionally plain strings so ads, social, messages, content, events, and\n referral programs remain peer packages. Allocation and schedule overrides\n use integer cents and dates.\n- **CampaignMetricSnapshot**: immutable period evidence with a global\n `dedupe_key` natural key. A snapshot always carries `campaignId`; a null\n `campaignChannelId` means campaign-level rollup evidence; when present, the\n channel must belong to that same campaign. API/MCP/CLI expose create/list/get\n only, and the save guard rejects hydrated edits, blind-id overwrites, and\n natural-key overwrites. Programmatic deletion is rejected.\n\nEvery model is `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\nAll generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.\n\n## Services\n\n- **CampaignLifecycleService** loads the current row before applying one legal\n transition. Paused campaigns may resume; completed campaigns may only be\n archived; archived campaigns are terminal.\n- **MetricIngestionService** validates required scope/source/period fields and\n delegates to `getOrCreateByDedupeKey()`. A replay returns the original row\n unchanged even if the replay payload differs.\n- **BudgetPacingService** computes campaign/channel pacing from snapshots and\n never persists derived balances. For each exact reporting period, campaign\n pacing prefers campaign rollups and otherwise sums channel snapshots,\n preventing double counting without dropping channel-only periods. Status\n uses a five-percent budget tolerance around schedule-derived expected spend.\n- **CampaignCollection** exposes bounded, tenant-and-Customer-scoped cursor\n pages ordered by `start_at DESC, id DESC` and bounded batch summaries for\n total count, active count, and latest start time. Customer scope is validated\n through runtime relationship metadata; there is no static commerce import or\n tenant-wide campaign materialization.\n\n## Svelte\n\n`@happyvertical/smrt-marketing/svelte` exports `MarketingDashboard`,\n`CampaignList`, `CampaignDetail`, `ChannelMix`, and `BudgetPacing`, plus view\ninterfaces and pure helpers. Components are props-only: no fetching, no model\nimports, no providers. Use Provider-free `smrt-ui` primitives, `--smrt-*`\ntokens, named `$props()` interfaces, and integer-cent inputs formatted only at\nrender time.\n\n## Boundaries and gotchas\n\n- Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and\n `smrt-ui`. Never statically import sibling domain packages.\n- `Campaign.customerId` targets `@happyvertical/smrt-commerce:Customer` through\n `@crossPackageRef`. Saves and customer-scoped reads require exact tenant\n agreement (including global-to-global only) and fail without revealing\n whether a Customer is absent or belongs elsewhere. Associated saves validate\n and persist through one transaction-bound Campaign instance; scoped reads\n validate and query through one fresh transaction-bound collection. PostgreSQL\n locks the validated Customer rows for the duration of each operation.\n- Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a\n campaign key in `sourceId`; marketing does not import or mutate Lead.\n- Attribution math remains in sales/referrals. Marketing stores performance\n evidence only.\n- Campaign rollups and channel snapshots may describe the same period. The\n pacing service deliberately chooses one evidence level instead of summing\n both.\n- Corrections append a new metric snapshot with a new dedupe key; never edit\n persisted evidence.\n- Table names are global: `campaigns`, `campaign_channels`, and\n `campaign_metric_snapshots`.\n"
1120
+ "agentDoc": "# @happyvertical/smrt-marketing\n\nCross-channel campaign coordination for SMRT. This package owns Campaign\nidentity, budgets, lifecycle, execution links, immutable performance evidence,\ncomputed pacing, and props-driven Svelte surfaces. It does not execute ads,\npublish social posts, send messages, or own sales attribution.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-marketing test` and\n`pnpm --filter @happyvertical/smrt-marketing typecheck`; use `test:coverage`\nto exercise the model/service and component coverage configurations.\nNatural-key changes must also run\n`pnpm --filter @happyvertical/smrt-marketing test:postgres` when\nthe disposable PostgreSQL harness is available. Use\n`pnpm --filter @happyvertical/smrt-marketing build` and `verify:pack` for\npublish-surface changes.\n\n## Models\n\n- **Campaign**: optional-tenant umbrella with stable natural key\n `(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,\n schedule, optional native UUID reference to the canonical commerce Customer,\n guarded metadata helpers, and lifecycle\n `draft → scheduled → active ↔ paused → completed → archived`. Raw saves are\n protected by an authoritative prior-status re-read; use\n `CampaignLifecycleService` for lifecycle writes.\n- **CampaignChannel**: one generic execution link per\n `(campaign_id, channel_kind, channel_ref)`. `channelKind`/`channelRef` are\n intentionally plain strings so ads, social, messages, content, events, and\n referral programs remain peer packages. Allocation and schedule overrides\n use integer cents and dates.\n- **CampaignMetricSnapshot**: immutable period evidence with a global\n `dedupe_key` natural key. A snapshot always carries `campaignId`; a null\n `campaignChannelId` means campaign-level rollup evidence; when present, the\n channel must belong to that same campaign. API/MCP/CLI expose create/list/get\n only, and the save guard rejects hydrated edits, blind-id overwrites, and\n natural-key overwrites. Programmatic deletion is rejected.\n\nEvery model is `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\nAll generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.\n\n## Services\n\n- **CampaignLifecycleService** loads the current row before applying one legal\n transition. Paused campaigns may resume; completed campaigns may only be\n archived; archived campaigns are terminal.\n- **MetricIngestionService** validates required scope/source/period fields and\n delegates to `getOrCreateByDedupeKey()`. A replay returns the original row\n unchanged even if the replay payload differs.\n- **BudgetPacingService** computes campaign/channel pacing from snapshots and\n never persists derived balances. For each exact reporting period, campaign\n pacing prefers campaign rollups and otherwise sums channel snapshots,\n preventing double counting without dropping channel-only periods. Status\n uses a five-percent budget tolerance around schedule-derived expected spend.\n- **CampaignCollection** exposes bounded, tenant-and-Customer-scoped cursor\n pages ordered by `start_at DESC, id DESC` and bounded batch summaries for\n total count, active count, and latest start time. Its reporting-page variant\n preserves that order/cursor and adds channel count/mix, immutable metric\n totals, and canonical pacing with two page-wide grouped reads. Customer scope\n is validated through runtime relationship metadata; there is no static\n commerce import, per-campaign callback, or tenant-wide materialization.\n\n## Svelte\n\n`@happyvertical/smrt-marketing/svelte` exports `MarketingDashboard`,\n`CampaignList`, `CampaignDetail`, `ChannelMix`, and `BudgetPacing`, plus view\ninterfaces and pure helpers. Components are props-only: no fetching, no model\nimports, no providers. Use Provider-free `smrt-ui` primitives, `--smrt-*`\ntokens, named `$props()` interfaces, and integer-cent inputs formatted only at\nrender time.\n\n## Boundaries and gotchas\n\n- Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and\n `smrt-ui`. Never statically import sibling domain packages.\n- `Campaign.customerId` targets `@happyvertical/smrt-commerce:Customer` through\n `@crossPackageRef`. Saves and customer-scoped reads require exact tenant\n agreement (including global-to-global only) and fail without revealing\n whether a Customer is absent or belongs elsewhere. Associated saves validate\n and persist through one transaction-bound Campaign instance; scoped reads\n validate and query through one fresh transaction-bound collection. PostgreSQL\n locks the validated Customer rows for the duration of each operation.\n- Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a\n campaign key in `sourceId`; marketing does not import or mutate Lead.\n- Attribution math remains in sales/referrals. Marketing stores performance\n evidence only.\n- Campaign rollups and channel snapshots may describe the same period. The\n pacing service deliberately chooses one evidence level instead of summing\n both.\n- Corrections append a new metric snapshot with a new dedupe key; never edit\n persisted evidence.\n- Table names are global: `campaigns`, `campaign_channels`, and\n `campaign_metric_snapshots`.\n"
1110
1121
  }
@@ -6,8 +6,11 @@ import type { BudgetPacingView } from '../types.js';
6
6
  import { cappedBudgetProgress, pacingStatusBadgeVariant } from '../types.js';
7
7
 
8
8
  export interface Props {
9
+ /** Budget pacing data to display. */
9
10
  pacing?: BudgetPacingView | null;
11
+ /** Locale for formatting currency and percentages. */
10
12
  locale?: string;
13
+ /** Heading text displayed above the progress bar. */
11
14
  label?: string;
12
15
  }
13
16
 
@@ -1,7 +1,10 @@
1
1
  import type { BudgetPacingView } from '../types.js';
2
2
  export interface Props {
3
+ /** Budget pacing data to display. */
3
4
  pacing?: BudgetPacingView | null;
5
+ /** Locale for formatting currency and percentages. */
4
6
  locale?: string;
7
+ /** Heading text displayed above the progress bar. */
5
8
  label?: string;
6
9
  }
7
10
  declare const BudgetPacing: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"BudgetPacing.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/BudgetPacing.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuED,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"BudgetPacing.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/BudgetPacing.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,MAAM,WAAW,KAAK;IACpB,qCAAqC;IACrC,MAAM,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuED,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}