@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 CHANGED
@@ -2,7 +2,7 @@ import { CAMPAIGN_CHANNEL_KINDS, CAMPAIGN_CHANNEL_STATUSES, CAMPAIGN_STATUSES }
2
2
  import { ObjectRegistry, SmrtCollection, SmrtObject, ValidationError, crossPackageRef, field, foreignKey, resolveListLimit, smrt } from "@happyvertical/smrt-core";
3
3
  import { TenantIsolationError, TenantScoped, getCurrentTenant, isSuperAdminBypass, isSystemContext, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
4
4
  //#region src/__smrt-register__.ts
5
- ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-marketing\",\"packageVersion\":\"0.43.9\",\"objects\":{\"@happyvertical/smrt-marketing:CampaignChannelCollection\":{\"name\":\"campaignchannelcollection\",\"className\":\"CampaignChannelCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignChannelCollection\",\"collection\":\"campaignchannels\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignChannelCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"findByCampaign\":{\"name\":\"findByCampaign\",\"async\":true,\"parameters\":[{\"name\":\"campaignId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignChannel[]>\",\"isStatic\":false,\"isPublic\":true},\"findByExecutionRef\":{\"name\":\"findByExecutionRef\",\"async\":true,\"parameters\":[{\"name\":\"channelKind\",\"type\":\"string\",\"optional\":false},{\"name\":\"channelRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignChannel[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_channels\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"CampaignChannel\",\"exportName\":\"CampaignChannelCollection\",\"collectionExportName\":\"CampaignChannelCollectionCollection\",\"schema\":{\"tableName\":\"campaign_channels\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_channels\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaign_channels_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaign_channels_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"b64ea87b\"}},\"@happyvertical/smrt-marketing:CampaignCollection\":{\"name\":\"campaigncollection\",\"className\":\"CampaignCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignCollection\",\"collection\":\"campaigns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"get\":{\"name\":\"get\",\"async\":true,\"parameters\":[{\"name\":\"filter\",\"type\":\"string | SmrtWhereClause<Campaign>\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<Campaign | null>\",\"isStatic\":false,\"isPublic\":true},\"list\":{\"name\":\"list\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"SmrtListOptions<Campaign>\",\"optional\":true}],\"returnType\":\"Promise<Campaign[] | Record<string>[]>\",\"isStatic\":false,\"isPublic\":true},\"findByCampaignKey\":{\"name\":\"findByCampaignKey\",\"async\":true,\"parameters\":[{\"name\":\"campaignKey\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":true}],\"returnType\":\"Promise<Campaign | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"CampaignStatus\",\"optional\":false}],\"returnType\":\"Promise<Campaign[]>\",\"isStatic\":false,\"isPublic\":true},\"listByCustomer\":{\"name\":\"listByCustomer\",\"async\":true,\"parameters\":[{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":false},{\"name\":\"customerId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"ListCampaignsByCustomerOptions\",\"optional\":true}],\"returnType\":\"Promise<CampaignCustomerPage>\",\"isStatic\":false,\"isPublic\":true},\"summarizeByCustomers\":{\"name\":\"summarizeByCustomers\",\"async\":true,\"parameters\":[{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":false},{\"name\":\"customerIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<CampaignCustomerSummary[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaigns\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Campaign\",\"exportName\":\"CampaignCollection\",\"collectionExportName\":\"CampaignCollectionCollection\",\"schema\":{\"tableName\":\"campaigns\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaigns\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaigns_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaigns_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"b1cd973d\"}},\"@happyvertical/smrt-marketing:CampaignMetricSnapshotCollection\":{\"name\":\"campaignmetricsnapshotcollection\",\"className\":\"CampaignMetricSnapshotCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignMetricSnapshotCollection\",\"collection\":\"campaignmetricsnapshots\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignMetricSnapshotCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"findByDedupeKey\":{\"name\":\"findByDedupeKey\",\"async\":true,\"parameters\":[{\"name\":\"dedupeKey\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot | null>\",\"isStatic\":false,\"isPublic\":true},\"getOrCreateByDedupeKey\":{\"name\":\"getOrCreateByDedupeKey\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"CampaignMetricSnapshotOptions\",\"optional\":false}],\"returnType\":\"Promise<object>\",\"isStatic\":false,\"isPublic\":true},\"findByCampaign\":{\"name\":\"findByCampaign\",\"async\":true,\"parameters\":[{\"name\":\"campaignId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot[]>\",\"isStatic\":false,\"isPublic\":true},\"findByChannel\":{\"name\":\"findByChannel\",\"async\":true,\"parameters\":[{\"name\":\"campaignChannelId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_metric_snapshots\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"CampaignMetricSnapshot\",\"exportName\":\"CampaignMetricSnapshotCollection\",\"collectionExportName\":\"CampaignMetricSnapshotCollectionCollection\",\"schema\":{\"tableName\":\"campaign_metric_snapshots\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_metric_snapshots\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaign_metric_snapshots_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaign_metric_snapshots_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"268285ee\"}},\"@happyvertical/smrt-marketing:Campaign\":{\"name\":\"campaign\",\"className\":\"Campaign\",\"qualifiedName\":\"@happyvertical/smrt-marketing:Campaign\",\"collection\":\"campaigns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/Campaign.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"customerId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Customer\",\"_meta\":{\"validate\":true,\"nullable\":true}},\"campaignKey\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"name\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"objective\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"status\":{\"type\":\"text\",\"required\":false,\"default\":\"draft\"},\"startAt\":{\"type\":\"datetime\",\"required\":false},\"endAt\":{\"type\":\"datetime\",\"required\":false},\"budgetCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"},\"metadata\":{\"type\":\"text\",\"required\":false,\"default\":\"{}\"}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"transitionTo\":{\"name\":\"transitionTo\",\"async\":false,\"parameters\":[{\"name\":\"next\",\"type\":\"CampaignStatus\",\"optional\":false}],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"metadata\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaigns\",\"conflictColumns\":[\"tenant_id\",\"campaign_key\"],\"indexes\":[{\"name\":\"campaigns_tenant_id_customer_id_start_at_id_idx\",\"columns\":[\"tenantId\",\"customerId\",\"startAt\",\"id\"]}],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"Campaign\",\"collectionExportName\":\"CampaignCollection\",\"validationRules\":[{\"field\":\"campaignKey\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"name\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaigns\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaigns\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"customer_id\\\" UUID,\\n \\\"campaign_key\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"objective\\\" TEXT DEFAULT '',\\n \\\"status\\\" TEXT DEFAULT 'draft',\\n \\\"start_at\\\" TIMESTAMP,\\n \\\"end_at\\\" TIMESTAMP,\\n \\\"budget_cents\\\" INTEGER DEFAULT 0,\\n \\\"currency\\\" TEXT DEFAULT 'USD',\\n \\\"metadata\\\" TEXT DEFAULT '{}'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"customer_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"campaign_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"objective\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"draft\"},\"start_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"end_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"budget_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"{}\"}},\"indexes\":[{\"name\":\"campaigns_tenant_id_campaign_key_idx\",\"columns\":[\"tenant_id\",\"campaign_key\"],\"unique\":true},{\"name\":\"campaigns_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaigns_tenant_id_customer_id_start_at_id_idx\",\"columns\":[\"tenant_id\",\"customer_id\",\"start_at\",\"id\"]},{\"name\":\"campaigns_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"campaigns_customer_id_idx\",\"columns\":[\"customer_id\"]}],\"version\":\"d0945102\"}},\"@happyvertical/smrt-marketing:CampaignChannel\":{\"name\":\"campaignchannel\",\"className\":\"CampaignChannel\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignChannel\",\"collection\":\"campaignchannels\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/CampaignChannel.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"campaignId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Campaign\",\"_meta\":{\"required\":true}},\"channelKind\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"channelRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"allocatedBudgetCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"startAt\":{\"type\":\"datetime\",\"required\":false},\"endAt\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":false,\"default\":\"planned\"}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_channels\",\"conflictColumns\":[\"campaign_id\",\"channel_kind\",\"channel_ref\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"CampaignChannel\",\"collectionExportName\":\"CampaignChannelCollection\",\"validationRules\":[{\"field\":\"campaignId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"channelKind\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"channelRef\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaign_channels\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_channels\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"campaign_id\\\" UUID NOT NULL,\\n \\\"channel_kind\\\" TEXT NOT NULL DEFAULT '',\\n \\\"channel_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"allocated_budget_cents\\\" INTEGER DEFAULT 0,\\n \\\"start_at\\\" TIMESTAMP,\\n \\\"end_at\\\" TIMESTAMP,\\n \\\"status\\\" TEXT DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"campaign_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"campaigns\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"channel_kind\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"channel_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"allocated_budget_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"start_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"end_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"campaign_channels_campaign_id_channel_kind_idx\",\"columns\":[\"campaign_id\",\"channel_kind\",\"channel_ref\"],\"unique\":true},{\"name\":\"campaign_channels_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaign_channels_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cc045525\"}},\"@happyvertical/smrt-marketing:CampaignMetricSnapshot\":{\"name\":\"campaignmetricsnapshot\",\"className\":\"CampaignMetricSnapshot\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignMetricSnapshot\",\"collection\":\"campaignmetricsnapshots\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/CampaignMetricSnapshot.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"campaignId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Campaign\",\"_meta\":{\"required\":true}},\"campaignChannelId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"CampaignChannel\",\"_meta\":{\"nullable\":true}},\"periodStart\":{\"type\":\"datetime\",\"required\":true,\"_meta\":{\"required\":true}},\"periodEnd\":{\"type\":\"datetime\",\"required\":true,\"_meta\":{\"required\":true}},\"spendCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"impressions\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"clicks\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"conversions\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"leads\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"revenueCents\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"source\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"dedupeKey\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_metric_snapshots\",\"conflictColumns\":[\"dedupe_key\"],\"api\":{\"include\":[\"list\",\"get\",\"create\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"CampaignMetricSnapshot\",\"collectionExportName\":\"CampaignMetricSnapshotCollection\",\"validationRules\":[{\"field\":\"campaignId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"periodStart\",\"rule\":\"required\",\"fieldType\":\"datetime\"},{\"field\":\"periodEnd\",\"rule\":\"required\",\"fieldType\":\"datetime\"},{\"field\":\"source\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"dedupeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaign_metric_snapshots\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_metric_snapshots\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"campaign_id\\\" UUID NOT NULL,\\n \\\"campaign_channel_id\\\" UUID,\\n \\\"period_start\\\" TIMESTAMP NOT NULL,\\n \\\"period_end\\\" TIMESTAMP NOT NULL,\\n \\\"spend_cents\\\" INTEGER DEFAULT 0,\\n \\\"impressions\\\" INTEGER DEFAULT 0,\\n \\\"clicks\\\" INTEGER DEFAULT 0,\\n \\\"conversions\\\" INTEGER DEFAULT 0,\\n \\\"leads\\\" INTEGER DEFAULT 0,\\n \\\"revenue_cents\\\" INTEGER,\\n \\\"source\\\" TEXT NOT NULL DEFAULT '',\\n \\\"dedupe_key\\\" TEXT NOT NULL DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"campaign_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"campaigns\",\"column\":\"id\",\"onDelete\":\"NO ACTION\",\"onUpdate\":\"CASCADE\"}},\"campaign_channel_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"campaign_channels\",\"column\":\"id\",\"onDelete\":\"NO ACTION\",\"onUpdate\":\"CASCADE\"}},\"period_start\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"unique\":false},\"period_end\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"unique\":false},\"spend_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"impressions\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"clicks\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"conversions\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"leads\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"revenue_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"source\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"dedupe_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"campaign_metric_snapshots_dedupe_key_idx\",\"columns\":[\"dedupe_key\"],\"unique\":true},{\"name\":\"campaign_metric_snapshots_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaign_metric_snapshots_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"campaign_metric_snapshots_campaign_id_idx\",\"columns\":[\"campaign_id\"]},{\"name\":\"campaign_metric_snapshots_campaign_channel_id_idx\",\"columns\":[\"campaign_channel_id\"]}],\"version\":\"1ccd36b3\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-commerce\",\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\"]}"));
5
+ ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-marketing\",\"packageVersion\":\"0.44.0\",\"objects\":{\"@happyvertical/smrt-marketing:CampaignChannelCollection\":{\"name\":\"campaignchannelcollection\",\"className\":\"CampaignChannelCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignChannelCollection\",\"collection\":\"campaignchannels\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignChannelCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"findByCampaign\":{\"name\":\"findByCampaign\",\"async\":true,\"parameters\":[{\"name\":\"campaignId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignChannel[]>\",\"isStatic\":false,\"isPublic\":true},\"findByExecutionRef\":{\"name\":\"findByExecutionRef\",\"async\":true,\"parameters\":[{\"name\":\"channelKind\",\"type\":\"string\",\"optional\":false},{\"name\":\"channelRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignChannel[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_channels\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"CampaignChannel\",\"exportName\":\"CampaignChannelCollection\",\"collectionExportName\":\"CampaignChannelCollectionCollection\",\"schema\":{\"tableName\":\"campaign_channels\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_channels\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaign_channels_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaign_channels_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"b64ea87b\"}},\"@happyvertical/smrt-marketing:CampaignCollection\":{\"name\":\"campaigncollection\",\"className\":\"CampaignCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignCollection\",\"collection\":\"campaigns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"get\":{\"name\":\"get\",\"async\":true,\"parameters\":[{\"name\":\"filter\",\"type\":\"string | SmrtWhereClause<Campaign>\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<Campaign | null>\",\"isStatic\":false,\"isPublic\":true},\"list\":{\"name\":\"list\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"SmrtListOptions<Campaign>\",\"optional\":true}],\"returnType\":\"Promise<Campaign[] | Record<string>[]>\",\"isStatic\":false,\"isPublic\":true},\"findByCampaignKey\":{\"name\":\"findByCampaignKey\",\"async\":true,\"parameters\":[{\"name\":\"campaignKey\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":true}],\"returnType\":\"Promise<Campaign | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"CampaignStatus\",\"optional\":false}],\"returnType\":\"Promise<Campaign[]>\",\"isStatic\":false,\"isPublic\":true},\"listByCustomer\":{\"name\":\"listByCustomer\",\"async\":true,\"parameters\":[{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":false},{\"name\":\"customerId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"ListCampaignsByCustomerOptions\",\"optional\":true}],\"returnType\":\"Promise<CampaignCustomerPage>\",\"isStatic\":false,\"isPublic\":true},\"listReportingByCustomer\":{\"name\":\"listReportingByCustomer\",\"async\":true,\"parameters\":[{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":false},{\"name\":\"customerId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"ListCampaignReportingByCustomerOptions\",\"optional\":true}],\"returnType\":\"Promise<CampaignReportingPage>\",\"isStatic\":false,\"isPublic\":true},\"summarizeByCustomers\":{\"name\":\"summarizeByCustomers\",\"async\":true,\"parameters\":[{\"name\":\"tenantId\",\"type\":\"string | null\",\"optional\":false},{\"name\":\"customerIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<CampaignCustomerSummary[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaigns\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Campaign\",\"exportName\":\"CampaignCollection\",\"collectionExportName\":\"CampaignCollectionCollection\",\"schema\":{\"tableName\":\"campaigns\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaigns\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaigns_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaigns_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"b1cd973d\"}},\"@happyvertical/smrt-marketing:CampaignMetricSnapshotCollection\":{\"name\":\"campaignmetricsnapshotcollection\",\"className\":\"CampaignMetricSnapshotCollection\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignMetricSnapshotCollection\",\"collection\":\"campaignmetricsnapshots\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/collections/CampaignMetricSnapshotCollection.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{},\"methods\":{\"findByDedupeKey\":{\"name\":\"findByDedupeKey\",\"async\":true,\"parameters\":[{\"name\":\"dedupeKey\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot | null>\",\"isStatic\":false,\"isPublic\":true},\"getOrCreateByDedupeKey\":{\"name\":\"getOrCreateByDedupeKey\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"CampaignMetricSnapshotOptions\",\"optional\":false}],\"returnType\":\"Promise<object>\",\"isStatic\":false,\"isPublic\":true},\"findByCampaign\":{\"name\":\"findByCampaign\",\"async\":true,\"parameters\":[{\"name\":\"campaignId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot[]>\",\"isStatic\":false,\"isPublic\":true},\"findByChannel\":{\"name\":\"findByChannel\",\"async\":true,\"parameters\":[{\"name\":\"campaignChannelId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<CampaignMetricSnapshot[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_metric_snapshots\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"CampaignMetricSnapshot\",\"exportName\":\"CampaignMetricSnapshotCollection\",\"collectionExportName\":\"CampaignMetricSnapshotCollectionCollection\",\"schema\":{\"tableName\":\"campaign_metric_snapshots\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_metric_snapshots\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"campaign_metric_snapshots_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true},{\"name\":\"campaign_metric_snapshots_created_at_idx\",\"columns\":[\"created_at\"]}],\"version\":\"268285ee\"}},\"@happyvertical/smrt-marketing:Campaign\":{\"name\":\"campaign\",\"className\":\"Campaign\",\"qualifiedName\":\"@happyvertical/smrt-marketing:Campaign\",\"collection\":\"campaigns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/Campaign.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"customerId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Customer\",\"_meta\":{\"validate\":true,\"nullable\":true}},\"campaignKey\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"name\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"objective\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"status\":{\"type\":\"text\",\"required\":false,\"default\":\"draft\"},\"startAt\":{\"type\":\"datetime\",\"required\":false},\"endAt\":{\"type\":\"datetime\",\"required\":false},\"budgetCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"},\"metadata\":{\"type\":\"text\",\"required\":false,\"default\":\"{}\"}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"transitionTo\":{\"name\":\"transitionTo\",\"async\":false,\"parameters\":[{\"name\":\"next\",\"type\":\"CampaignStatus\",\"optional\":false}],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"metadata\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaigns\",\"conflictColumns\":[\"tenant_id\",\"campaign_key\"],\"indexes\":[{\"name\":\"campaigns_tenant_id_customer_id_start_at_id_idx\",\"columns\":[\"tenantId\",\"customerId\",\"startAt\",\"id\"]}],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"Campaign\",\"collectionExportName\":\"CampaignCollection\",\"validationRules\":[{\"field\":\"campaignKey\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"name\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaigns\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaigns\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"customer_id\\\" UUID,\\n \\\"campaign_key\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"objective\\\" TEXT DEFAULT '',\\n \\\"status\\\" TEXT DEFAULT 'draft',\\n \\\"start_at\\\" TIMESTAMP,\\n \\\"end_at\\\" TIMESTAMP,\\n \\\"budget_cents\\\" INTEGER DEFAULT 0,\\n \\\"currency\\\" TEXT DEFAULT 'USD',\\n \\\"metadata\\\" TEXT DEFAULT '{}'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"customer_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"campaign_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"objective\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"draft\"},\"start_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"end_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"budget_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"{}\"}},\"indexes\":[{\"name\":\"campaigns_tenant_id_campaign_key_idx\",\"columns\":[\"tenant_id\",\"campaign_key\"],\"unique\":true},{\"name\":\"campaigns_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaigns_tenant_id_customer_id_start_at_id_idx\",\"columns\":[\"tenant_id\",\"customer_id\",\"start_at\",\"id\"]},{\"name\":\"campaigns_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"campaigns_customer_id_idx\",\"columns\":[\"customer_id\"]}],\"version\":\"d0945102\"}},\"@happyvertical/smrt-marketing:CampaignChannel\":{\"name\":\"campaignchannel\",\"className\":\"CampaignChannel\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignChannel\",\"collection\":\"campaignchannels\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/CampaignChannel.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"campaignId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Campaign\",\"_meta\":{\"required\":true}},\"channelKind\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"channelRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"allocatedBudgetCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"startAt\":{\"type\":\"datetime\",\"required\":false},\"endAt\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":false,\"default\":\"planned\"}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_channels\",\"conflictColumns\":[\"campaign_id\",\"channel_kind\",\"channel_ref\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"CampaignChannel\",\"collectionExportName\":\"CampaignChannelCollection\",\"validationRules\":[{\"field\":\"campaignId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"channelKind\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"channelRef\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaign_channels\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_channels\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"campaign_id\\\" UUID NOT NULL,\\n \\\"channel_kind\\\" TEXT NOT NULL DEFAULT '',\\n \\\"channel_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"allocated_budget_cents\\\" INTEGER DEFAULT 0,\\n \\\"start_at\\\" TIMESTAMP,\\n \\\"end_at\\\" TIMESTAMP,\\n \\\"status\\\" TEXT DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"campaign_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"campaigns\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"channel_kind\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"channel_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"allocated_budget_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"start_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"end_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"campaign_channels_campaign_id_channel_kind_idx\",\"columns\":[\"campaign_id\",\"channel_kind\",\"channel_ref\"],\"unique\":true},{\"name\":\"campaign_channels_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaign_channels_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cc045525\"}},\"@happyvertical/smrt-marketing:CampaignMetricSnapshot\":{\"name\":\"campaignmetricsnapshot\",\"className\":\"CampaignMetricSnapshot\",\"qualifiedName\":\"@happyvertical/smrt-marketing:CampaignMetricSnapshot\",\"collection\":\"campaignmetricsnapshots\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/marketing/src/models/CampaignMetricSnapshot.ts\",\"packageName\":\"@happyvertical/smrt-marketing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"campaignId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Campaign\",\"_meta\":{\"required\":true}},\"campaignChannelId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"CampaignChannel\",\"_meta\":{\"nullable\":true}},\"periodStart\":{\"type\":\"datetime\",\"required\":true,\"_meta\":{\"required\":true}},\"periodEnd\":{\"type\":\"datetime\",\"required\":true,\"_meta\":{\"required\":true}},\"spendCents\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"impressions\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"clicks\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"conversions\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"leads\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"revenueCents\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"source\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"dedupeKey\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}}},\"methods\":{\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"campaign_metric_snapshots\",\"conflictColumns\":[\"dedupe_key\"],\"api\":{\"include\":[\"list\",\"get\",\"create\"]},\"mcp\":{\"include\":[\"list\",\"get\",\"create\"]},\"cli\":{\"include\":[\"list\",\"get\",\"create\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"CampaignMetricSnapshot\",\"collectionExportName\":\"CampaignMetricSnapshotCollection\",\"validationRules\":[{\"field\":\"campaignId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"periodStart\",\"rule\":\"required\",\"fieldType\":\"datetime\"},{\"field\":\"periodEnd\",\"rule\":\"required\",\"fieldType\":\"datetime\"},{\"field\":\"source\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"dedupeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"campaign_metric_snapshots\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"campaign_metric_snapshots\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"campaign_id\\\" UUID NOT NULL,\\n \\\"campaign_channel_id\\\" UUID,\\n \\\"period_start\\\" TIMESTAMP NOT NULL,\\n \\\"period_end\\\" TIMESTAMP NOT NULL,\\n \\\"spend_cents\\\" INTEGER DEFAULT 0,\\n \\\"impressions\\\" INTEGER DEFAULT 0,\\n \\\"clicks\\\" INTEGER DEFAULT 0,\\n \\\"conversions\\\" INTEGER DEFAULT 0,\\n \\\"leads\\\" INTEGER DEFAULT 0,\\n \\\"revenue_cents\\\" INTEGER,\\n \\\"source\\\" TEXT NOT NULL DEFAULT '',\\n \\\"dedupe_key\\\" TEXT NOT NULL DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"campaign_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"campaigns\",\"column\":\"id\",\"onDelete\":\"NO ACTION\",\"onUpdate\":\"CASCADE\"}},\"campaign_channel_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"campaign_channels\",\"column\":\"id\",\"onDelete\":\"NO ACTION\",\"onUpdate\":\"CASCADE\"}},\"period_start\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"unique\":false},\"period_end\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"unique\":false},\"spend_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"impressions\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"clicks\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"conversions\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"leads\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"revenue_cents\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"source\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"dedupe_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"campaign_metric_snapshots_dedupe_key_idx\",\"columns\":[\"dedupe_key\"],\"unique\":true},{\"name\":\"campaign_metric_snapshots_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"campaign_metric_snapshots_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"campaign_metric_snapshots_campaign_id_idx\",\"columns\":[\"campaign_id\"]},{\"name\":\"campaign_metric_snapshots_campaign_channel_id_idx\",\"columns\":[\"campaign_channel_id\"]}],\"version\":\"1ccd36b3\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-commerce\",\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\"]}"));
6
6
  //#endregion
7
7
  //#region src/errors.ts
8
8
  var CampaignCustomerScopeError = class extends Error {
@@ -375,6 +375,255 @@ var CampaignChannelCollection = class extends SmrtCollection {
375
375
  }
376
376
  };
377
377
  //#endregion
378
+ //#region src/services/pacing-calculation.ts
379
+ function calculateCampaignPacing(campaign, evidence, at = /* @__PURE__ */ new Date()) {
380
+ return calculatePacing({
381
+ campaign,
382
+ evidence,
383
+ budgetCents: campaign.budgetCents,
384
+ startAt: campaign.startAt,
385
+ endAt: campaign.endAt,
386
+ at
387
+ });
388
+ }
389
+ function calculatePacing({ campaign, evidence, budgetCents, startAt, endAt, at }) {
390
+ const elapsed = elapsedFraction(startAt, endAt, at);
391
+ const expectedSpendCents = elapsed === null ? null : Math.round(budgetCents * elapsed);
392
+ const varianceCents = expectedSpendCents === null ? null : evidence.spendCents - expectedSpendCents;
393
+ return {
394
+ campaignId: campaign.id ?? "",
395
+ currency: campaign.currency,
396
+ budgetCents,
397
+ spendCents: evidence.spendCents,
398
+ remainingCents: budgetCents - evidence.spendCents,
399
+ expectedSpendCents,
400
+ varianceCents,
401
+ budgetFraction: budgetCents > 0 ? evidence.spendCents / budgetCents : null,
402
+ elapsedFraction: elapsed,
403
+ status: pacingStatus({
404
+ campaign,
405
+ budgetCents,
406
+ spendCents: evidence.spendCents,
407
+ elapsedFraction: elapsed,
408
+ varianceCents,
409
+ startAt,
410
+ at
411
+ }),
412
+ snapshotCount: evidence.snapshotCount,
413
+ usedCampaignRollups: evidence.usedCampaignRollups
414
+ };
415
+ }
416
+ function pacingStatus({ campaign, budgetCents, spendCents, elapsedFraction: elapsedFraction2, varianceCents, startAt, at }) {
417
+ if (budgetCents <= 0) return "unbudgeted";
418
+ if (spendCents > budgetCents) return "over_budget";
419
+ if (startAt && at.getTime() < startAt.getTime()) return "not_started";
420
+ if (campaign.status === "completed" || campaign.status === "archived" || elapsedFraction2 === 1) return "complete";
421
+ if (varianceCents === null) return "on_track";
422
+ const toleranceCents = Math.max(1, Math.round(budgetCents * .05));
423
+ if (varianceCents > toleranceCents) return "ahead";
424
+ if (varianceCents < -toleranceCents) return "behind";
425
+ return "on_track";
426
+ }
427
+ function elapsedFraction(startAt, endAt, at) {
428
+ if (!startAt || !endAt) return null;
429
+ const start = startAt.getTime();
430
+ const end = endAt.getTime();
431
+ if (end <= start) return at.getTime() < start ? 0 : 1;
432
+ return Math.min(1, Math.max(0, (at.getTime() - start) / (end - start)));
433
+ }
434
+ //#endregion
435
+ //#region src/models/CampaignMetricSnapshot.ts
436
+ var __defProp = Object.defineProperty;
437
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
438
+ var __decorateClass = (decorators, target, key, kind) => {
439
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
440
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
441
+ if (kind && result) __defProp(target, key, result);
442
+ return result;
443
+ };
444
+ var persistedSnapshotState = /* @__PURE__ */ new WeakMap();
445
+ var CampaignMetricSnapshot = class extends SmrtObject {
446
+ tenantId = null;
447
+ campaignId = "";
448
+ campaignChannelId = null;
449
+ periodStart = /* @__PURE__ */ new Date();
450
+ periodEnd = /* @__PURE__ */ new Date();
451
+ spendCents = 0;
452
+ impressions = 0;
453
+ clicks = 0;
454
+ conversions = 0;
455
+ leads = 0;
456
+ revenueCents = null;
457
+ source = "";
458
+ dedupeKey = "";
459
+ constructor(options = {}) {
460
+ super(options);
461
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
462
+ if (options.campaignId !== void 0) this.campaignId = options.campaignId;
463
+ if (options.campaignChannelId !== void 0) this.campaignChannelId = options.campaignChannelId;
464
+ if (options.periodStart !== void 0) this.periodStart = CampaignMetricSnapshot.coerceDate(options.periodStart) ?? /* @__PURE__ */ new Date(NaN);
465
+ if (options.periodEnd !== void 0) this.periodEnd = CampaignMetricSnapshot.coerceDate(options.periodEnd) ?? /* @__PURE__ */ new Date(NaN);
466
+ if (options.spendCents !== void 0) this.spendCents = options.spendCents;
467
+ if (options.impressions !== void 0) this.impressions = options.impressions;
468
+ if (options.clicks !== void 0) this.clicks = options.clicks;
469
+ if (options.conversions !== void 0) this.conversions = options.conversions;
470
+ if (options.leads !== void 0) this.leads = options.leads;
471
+ if (options.revenueCents !== void 0) this.revenueCents = options.revenueCents;
472
+ if (options.source !== void 0) this.source = options.source;
473
+ if (options.dedupeKey !== void 0) this.dedupeKey = options.dedupeKey;
474
+ }
475
+ async initialize() {
476
+ await super.initialize();
477
+ this.periodStart = CampaignMetricSnapshot.coerceDate(this.periodStart) ?? /* @__PURE__ */ new Date(NaN);
478
+ this.periodEnd = CampaignMetricSnapshot.coerceDate(this.periodEnd) ?? /* @__PURE__ */ new Date(NaN);
479
+ if (this.isPersisted) persistedSnapshotState.set(this, this.serializeState());
480
+ return this;
481
+ }
482
+ async save() {
483
+ this.assertMetrics();
484
+ await this.assertChannelMatchesCampaign();
485
+ const captured = persistedSnapshotState.get(this);
486
+ if (captured !== void 0) {
487
+ if (captured !== this.serializeState()) throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence; ingest a correcting snapshot instead.`);
488
+ } else if (this.id && await this.isSaved()) throw new Error(`CampaignMetricSnapshot ${this.id}: refusing to overwrite an existing snapshot from a non-hydrated instance; snapshots are immutable evidence.`);
489
+ else if (this.dedupeKey) try {
490
+ const row = await this.db.get(this.tableName, { dedupe_key: this.dedupeKey });
491
+ if (row && row.id !== this.id) throw new Error(`CampaignMetricSnapshot (dedupeKey '${this.dedupeKey}'): a snapshot with this dedupe key already exists; use MetricIngestionService for idempotent ingestion.`);
492
+ } catch (error) {
493
+ if (error instanceof Error && error.message.includes("idempotent ingestion")) throw error;
494
+ }
495
+ const result = await super.save();
496
+ persistedSnapshotState.set(this, this.serializeState());
497
+ return result;
498
+ }
499
+ async delete() {
500
+ throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence and cannot be deleted.`);
501
+ }
502
+ assertMetrics() {
503
+ if (Number.isNaN(this.periodStart.getTime()) || Number.isNaN(this.periodEnd.getTime())) throw new Error("CampaignMetricSnapshot periodStart and periodEnd must be valid dates.");
504
+ if (this.periodEnd.getTime() < this.periodStart.getTime()) throw new Error("CampaignMetricSnapshot periodEnd must be on or after periodStart.");
505
+ const integerMetrics = [
506
+ ["spendCents", this.spendCents],
507
+ ["impressions", this.impressions],
508
+ ["clicks", this.clicks],
509
+ ["conversions", this.conversions],
510
+ ["leads", this.leads]
511
+ ];
512
+ for (const [name, value] of integerMetrics) if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer.`);
513
+ if (this.revenueCents !== null && (!Number.isInteger(this.revenueCents) || this.revenueCents < 0)) throw new Error("revenueCents must be null or a non-negative integer.");
514
+ }
515
+ async assertChannelMatchesCampaign() {
516
+ if (!this.campaignChannelId) return;
517
+ if ((await this.loadRelated("campaignChannelId")).campaignId !== this.campaignId) throw new Error(`CampaignMetricSnapshot campaignChannelId '${this.campaignChannelId}' does not belong to campaignId '${this.campaignId}'.`);
518
+ }
519
+ serializeState() {
520
+ return JSON.stringify({
521
+ tenantId: this.tenantId,
522
+ campaignId: this.campaignId,
523
+ campaignChannelId: this.campaignChannelId,
524
+ periodStart: this.periodStart.toISOString(),
525
+ periodEnd: this.periodEnd.toISOString(),
526
+ spendCents: this.spendCents,
527
+ impressions: this.impressions,
528
+ clicks: this.clicks,
529
+ conversions: this.conversions,
530
+ leads: this.leads,
531
+ revenueCents: this.revenueCents,
532
+ source: this.source,
533
+ dedupeKey: this.dedupeKey
534
+ });
535
+ }
536
+ static coerceDate(value) {
537
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
538
+ if (typeof value === "number" || typeof value === "string") {
539
+ const date = new Date(value);
540
+ return Number.isNaN(date.getTime()) ? null : date;
541
+ }
542
+ return null;
543
+ }
544
+ };
545
+ __decorateClass([tenantId({ nullable: true })], CampaignMetricSnapshot.prototype, "tenantId", 2);
546
+ __decorateClass([foreignKey(Campaign, { required: true })], CampaignMetricSnapshot.prototype, "campaignId", 2);
547
+ __decorateClass([foreignKey(CampaignChannel, { nullable: true })], CampaignMetricSnapshot.prototype, "campaignChannelId", 2);
548
+ __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "periodStart", 2);
549
+ __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "periodEnd", 2);
550
+ __decorateClass([field({
551
+ type: "integer",
552
+ nullable: true
553
+ })], CampaignMetricSnapshot.prototype, "revenueCents", 2);
554
+ __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "source", 2);
555
+ __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "dedupeKey", 2);
556
+ CampaignMetricSnapshot = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
557
+ tableName: "campaign_metric_snapshots",
558
+ conflictColumns: ["dedupe_key"],
559
+ api: { include: [
560
+ "list",
561
+ "get",
562
+ "create"
563
+ ] },
564
+ mcp: { include: [
565
+ "list",
566
+ "get",
567
+ "create"
568
+ ] },
569
+ cli: { include: [
570
+ "list",
571
+ "get",
572
+ "create"
573
+ ] }
574
+ })], CampaignMetricSnapshot);
575
+ //#endregion
576
+ //#region src/collections/CampaignMetricSnapshotCollection.ts
577
+ var CampaignMetricSnapshotCollection = class extends SmrtCollection {
578
+ static _itemClass = CampaignMetricSnapshot;
579
+ async findByDedupeKey(dedupeKey) {
580
+ if (!dedupeKey) return null;
581
+ return (await this.list({
582
+ where: { dedupeKey },
583
+ limit: 1
584
+ }))[0] ?? null;
585
+ }
586
+ async getOrCreateByDedupeKey(options) {
587
+ const dedupeKey = options.dedupeKey ?? "";
588
+ if (!dedupeKey) throw new Error("CampaignMetricSnapshotCollection.getOrCreateByDedupeKey requires a dedupeKey.");
589
+ const existing = await this.findByDedupeKey(dedupeKey);
590
+ if (existing) return {
591
+ snapshot: existing,
592
+ created: false
593
+ };
594
+ try {
595
+ return {
596
+ snapshot: await this.create({
597
+ ...options,
598
+ periodStart: options.periodStart,
599
+ periodEnd: options.periodEnd,
600
+ _insertOnly: true
601
+ }),
602
+ created: true
603
+ };
604
+ } catch (error) {
605
+ const concurrent = await this.findByDedupeKey(dedupeKey);
606
+ if (concurrent) return {
607
+ snapshot: concurrent,
608
+ created: false
609
+ };
610
+ throw error;
611
+ }
612
+ }
613
+ async findByCampaign(campaignId) {
614
+ return await this.list({
615
+ where: { campaignId },
616
+ orderBy: "period_start ASC"
617
+ });
618
+ }
619
+ async findByChannel(campaignChannelId) {
620
+ return await this.list({
621
+ where: { campaignChannelId },
622
+ orderBy: "period_start ASC"
623
+ });
624
+ }
625
+ };
626
+ //#endregion
378
627
  //#region src/collections/CampaignCollection.ts
379
628
  var MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE = 100;
380
629
  var MAX_CAMPAIGN_CUSTOMER_BATCH_SIZE = 100;
@@ -416,10 +665,133 @@ var CampaignCollection = class CampaignCollection extends SmrtCollection {
416
665
  if (limit > 100) throw new Error(`Campaign customer page limit must not exceed 100.`);
417
666
  if (limit < 1) throw new Error("Campaign customer page limit must be at least 1.");
418
667
  const cursor = normalizeCursor(options.after);
419
- return withCanonicalTenantContext(normalizedTenantId, () => this.inCustomerReadTransaction(async (bound) => bound.listByCustomerInTransaction(normalizedTenantId, normalizedCustomerId, limit, cursor)));
668
+ return withCanonicalTenantContext(normalizedTenantId, () => this.inCustomerReadTransaction(async (bound) => bound.listByCustomerInTransaction(normalizedTenantId, normalizedCustomerId, limit, cursor, "CampaignCollection.listByCustomer")));
420
669
  }
421
- async listByCustomerInTransaction(tenantId, customerId, limit, cursor) {
422
- await assertCustomersBelongToTenant(this.options, tenantId, [customerId], "CampaignCollection.listByCustomer", "share");
670
+ /**
671
+ * Return one bounded customer page with its complete read-only reporting
672
+ * projection. Channels and immutable evidence are aggregated in two grouped
673
+ * reads, independent of page size; no campaign callback or lazy load runs.
674
+ */
675
+ async listReportingByCustomer(tenantId, customerId, options = {}) {
676
+ const normalizedTenantId = normalizeTenantScope(tenantId, "CampaignCollection.listReportingByCustomer");
677
+ const normalizedCustomerId = normalizeUuid(customerId, "customerId");
678
+ const limit = resolveListLimit(options.limit, {
679
+ defaultValue: 50,
680
+ maxValue: Number.MAX_SAFE_INTEGER,
681
+ parameterName: "campaign reporting page limit"
682
+ });
683
+ if (limit > 100) throw new Error(`Campaign reporting page limit must not exceed 100.`);
684
+ if (limit < 1) throw new Error("Campaign reporting page limit must be at least 1.");
685
+ const cursor = normalizeCursor(options.after);
686
+ const at = normalizeReportingAt(options.at);
687
+ return withCanonicalTenantContext(normalizedTenantId, () => this.inCustomerReadTransaction(async (bound) => {
688
+ const page = await bound.listByCustomerInTransaction(normalizedTenantId, normalizedCustomerId, limit, cursor, "CampaignCollection.listReportingByCustomer");
689
+ return bound.projectReportingPage(normalizedTenantId, page, at);
690
+ }));
691
+ }
692
+ async projectReportingPage(tenantId, page, at) {
693
+ const campaignIds = page.items.map((campaign) => {
694
+ if (!campaign.id) throw new Error("Campaign reporting requires persisted Campaign rows.");
695
+ return normalizeUuid(campaign.id, "campaign id");
696
+ });
697
+ if (campaignIds.length === 0) return {
698
+ items: [],
699
+ nextCursor: page.nextCursor
700
+ };
701
+ const channelRows = await this.loadChannelMixRows(tenantId, campaignIds);
702
+ const metricRows = await this.loadMetricRows(tenantId, campaignIds);
703
+ const channelsByCampaign = groupChannelMix(channelRows);
704
+ const metricsByCampaign = groupMetricTotals(metricRows);
705
+ return {
706
+ items: page.items.map((campaign) => {
707
+ const campaignId = campaign.id ?? "";
708
+ const channelMix = channelsByCampaign.get(campaignId) ?? [];
709
+ const metrics = metricsByCampaign.get(campaignId) ?? emptyMetricTotals();
710
+ return {
711
+ campaign,
712
+ channelCount: channelMix.reduce((sum, entry) => checkedAdd(sum, entry.count, "Campaign channel count"), 0),
713
+ channelMix,
714
+ metricTotals: metrics.totals,
715
+ pacing: calculateCampaignPacing(campaign, {
716
+ spendCents: metrics.totals.spendCents,
717
+ snapshotCount: metrics.snapshotCount,
718
+ usedCampaignRollups: metrics.usedCampaignRollups
719
+ }, at)
720
+ };
721
+ }),
722
+ nextCursor: page.nextCursor
723
+ };
724
+ }
725
+ async loadChannelMixRows(tenantId, campaignIds) {
726
+ const channels = await CampaignChannelCollection.create({
727
+ ...this.options,
728
+ db: this.db,
729
+ defaultListLimit: void 0,
730
+ maxListLimit: void 0
731
+ });
732
+ const db = requireDatabase(this.options);
733
+ const placeholders = campaignIds.map(() => "?").join(", ");
734
+ const tenantPredicate = tenantId === null ? "tenant_id IS NULL" : "tenant_id = ?";
735
+ const tenantParams = tenantId === null ? [] : [tenantId];
736
+ return (await db.query(`SELECT campaign_id, channel_kind, COUNT(*) AS channel_count
737
+ FROM ${channels.tableName}
738
+ WHERE ${tenantPredicate} AND campaign_id IN (${placeholders})
739
+ GROUP BY campaign_id, channel_kind
740
+ ORDER BY campaign_id ASC, channel_kind ASC`, ...tenantParams, ...campaignIds)).rows;
741
+ }
742
+ async loadMetricRows(tenantId, campaignIds) {
743
+ const snapshots = await CampaignMetricSnapshotCollection.create({
744
+ ...this.options,
745
+ db: this.db,
746
+ defaultListLimit: void 0,
747
+ maxListLimit: void 0
748
+ });
749
+ const db = requireDatabase(this.options);
750
+ const placeholders = campaignIds.map(() => "?").join(", ");
751
+ const tenantPredicate = tenantId === null ? "tenant_id IS NULL" : "tenant_id = ?";
752
+ const tenantParams = tenantId === null ? [] : [tenantId];
753
+ return (await db.query(`WITH scoped_snapshots AS (
754
+ SELECT campaign_id,
755
+ campaign_channel_id,
756
+ period_start,
757
+ period_end,
758
+ spend_cents,
759
+ impressions,
760
+ clicks,
761
+ conversions,
762
+ leads,
763
+ revenue_cents
764
+ FROM ${snapshots.tableName}
765
+ WHERE ${tenantPredicate} AND campaign_id IN (${placeholders})
766
+ ), rollup_periods AS (
767
+ SELECT DISTINCT campaign_id, period_start, period_end
768
+ FROM scoped_snapshots
769
+ WHERE campaign_channel_id IS NULL
770
+ ), selected_snapshots AS (
771
+ SELECT s.*
772
+ FROM scoped_snapshots s
773
+ LEFT JOIN rollup_periods r
774
+ ON r.campaign_id = s.campaign_id
775
+ AND r.period_start = s.period_start
776
+ AND r.period_end = s.period_end
777
+ WHERE s.campaign_channel_id IS NULL OR r.campaign_id IS NULL
778
+ )
779
+ SELECT campaign_id,
780
+ COUNT(*) AS snapshot_count,
781
+ MAX(CASE WHEN campaign_channel_id IS NULL THEN 1 ELSE 0 END)
782
+ AS used_campaign_rollups,
783
+ SUM(spend_cents) AS spend_cents,
784
+ SUM(impressions) AS impressions,
785
+ SUM(clicks) AS clicks,
786
+ SUM(conversions) AS conversions,
787
+ SUM(leads) AS leads,
788
+ SUM(COALESCE(revenue_cents, 0)) AS revenue_cents
789
+ FROM selected_snapshots
790
+ GROUP BY campaign_id
791
+ ORDER BY campaign_id ASC`, ...tenantParams, ...campaignIds)).rows;
792
+ }
793
+ async listByCustomerInTransaction(tenantId, customerId, limit, cursor, operation) {
794
+ await assertCustomersBelongToTenant(this.options, tenantId, [customerId], operation, "share");
423
795
  const items = cursor?.startAt === null ? await this.listNullStartLane(tenantId, customerId, limit + 1, cursor) : await this.listScheduledLane(tenantId, customerId, limit, cursor);
424
796
  const hasMore = items.length > limit;
425
797
  const pageItems = hasMore ? items.slice(0, limit) : items;
@@ -516,9 +888,63 @@ var CampaignCollection = class CampaignCollection extends SmrtCollection {
516
888
  });
517
889
  }
518
890
  };
891
+ function groupChannelMix(rows) {
892
+ const grouped = /* @__PURE__ */ new Map();
893
+ for (const row of rows) {
894
+ const campaignId = normalizeUuid(String(row.campaign_id), "campaign id");
895
+ const channelKind = String(row.channel_kind);
896
+ if (!channelKind) throw new Error("Campaign channel kind is empty.");
897
+ const entry = {
898
+ channelKind,
899
+ count: toSafeCount(row.channel_count, "Campaign channel count")
900
+ };
901
+ const entries = grouped.get(campaignId) ?? [];
902
+ entries.push(entry);
903
+ grouped.set(campaignId, entries);
904
+ }
905
+ return grouped;
906
+ }
907
+ function groupMetricTotals(rows) {
908
+ return new Map(rows.map((row) => {
909
+ return [normalizeUuid(String(row.campaign_id), "campaign id"), {
910
+ totals: {
911
+ spendCents: toSafeCount(row.spend_cents, "Campaign spend total"),
912
+ impressions: toSafeCount(row.impressions, "Campaign impression total"),
913
+ clicks: toSafeCount(row.clicks, "Campaign click total"),
914
+ conversions: toSafeCount(row.conversions, "Campaign conversion total"),
915
+ leads: toSafeCount(row.leads, "Campaign lead total"),
916
+ revenueCents: toSafeCount(row.revenue_cents, "Campaign revenue total")
917
+ },
918
+ snapshotCount: toSafeCount(row.snapshot_count, "Campaign snapshot count"),
919
+ usedCampaignRollups: toBooleanFlag(row.used_campaign_rollups)
920
+ }];
921
+ }));
922
+ }
923
+ function emptyMetricTotals() {
924
+ return {
925
+ totals: {
926
+ spendCents: 0,
927
+ impressions: 0,
928
+ clicks: 0,
929
+ conversions: 0,
930
+ leads: 0,
931
+ revenueCents: 0
932
+ },
933
+ snapshotCount: 0,
934
+ usedCampaignRollups: false
935
+ };
936
+ }
937
+ function toBooleanFlag(value) {
938
+ return value === true || value === 1 || value === "1";
939
+ }
940
+ function checkedAdd(left, right, label) {
941
+ const sum = left + right;
942
+ if (!Number.isSafeInteger(sum) || sum < 0) throw new Error(`${label} is outside the safe integer range.`);
943
+ return sum;
944
+ }
519
945
  function requireDatabase(options) {
520
946
  const db = options.db;
521
- if (!db || typeof db !== "object" || !("query" in db)) throw new Error("Campaign customer summaries require an initialized database connection.");
947
+ if (!db || typeof db !== "object" || !("query" in db)) throw new Error("Campaign reporting and summaries require an initialized database connection.");
522
948
  return db;
523
949
  }
524
950
  function coerceDate(value) {
@@ -545,6 +971,12 @@ function normalizeCursor(cursor) {
545
971
  startAt
546
972
  };
547
973
  }
974
+ function normalizeReportingAt(value) {
975
+ if (value === void 0) return /* @__PURE__ */ new Date();
976
+ const at = coerceDate(value);
977
+ if (!at) throw new Error("campaign reporting at is invalid.");
978
+ return at;
979
+ }
548
980
  function cursorFromCampaign(campaign) {
549
981
  if (!campaign?.id) return null;
550
982
  return {
@@ -616,198 +1048,6 @@ function normalizeCampaignUuidWhere(where) {
616
1048
  return normalized;
617
1049
  }
618
1050
  //#endregion
619
- //#region src/models/CampaignMetricSnapshot.ts
620
- var __defProp = Object.defineProperty;
621
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
622
- var __decorateClass = (decorators, target, key, kind) => {
623
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
624
- for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
625
- if (kind && result) __defProp(target, key, result);
626
- return result;
627
- };
628
- var persistedSnapshotState = /* @__PURE__ */ new WeakMap();
629
- var CampaignMetricSnapshot = class extends SmrtObject {
630
- tenantId = null;
631
- campaignId = "";
632
- campaignChannelId = null;
633
- periodStart = /* @__PURE__ */ new Date();
634
- periodEnd = /* @__PURE__ */ new Date();
635
- spendCents = 0;
636
- impressions = 0;
637
- clicks = 0;
638
- conversions = 0;
639
- leads = 0;
640
- revenueCents = null;
641
- source = "";
642
- dedupeKey = "";
643
- constructor(options = {}) {
644
- super(options);
645
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
646
- if (options.campaignId !== void 0) this.campaignId = options.campaignId;
647
- if (options.campaignChannelId !== void 0) this.campaignChannelId = options.campaignChannelId;
648
- if (options.periodStart !== void 0) this.periodStart = CampaignMetricSnapshot.coerceDate(options.periodStart) ?? /* @__PURE__ */ new Date(NaN);
649
- if (options.periodEnd !== void 0) this.periodEnd = CampaignMetricSnapshot.coerceDate(options.periodEnd) ?? /* @__PURE__ */ new Date(NaN);
650
- if (options.spendCents !== void 0) this.spendCents = options.spendCents;
651
- if (options.impressions !== void 0) this.impressions = options.impressions;
652
- if (options.clicks !== void 0) this.clicks = options.clicks;
653
- if (options.conversions !== void 0) this.conversions = options.conversions;
654
- if (options.leads !== void 0) this.leads = options.leads;
655
- if (options.revenueCents !== void 0) this.revenueCents = options.revenueCents;
656
- if (options.source !== void 0) this.source = options.source;
657
- if (options.dedupeKey !== void 0) this.dedupeKey = options.dedupeKey;
658
- }
659
- async initialize() {
660
- await super.initialize();
661
- this.periodStart = CampaignMetricSnapshot.coerceDate(this.periodStart) ?? /* @__PURE__ */ new Date(NaN);
662
- this.periodEnd = CampaignMetricSnapshot.coerceDate(this.periodEnd) ?? /* @__PURE__ */ new Date(NaN);
663
- if (this.isPersisted) persistedSnapshotState.set(this, this.serializeState());
664
- return this;
665
- }
666
- async save() {
667
- this.assertMetrics();
668
- await this.assertChannelMatchesCampaign();
669
- const captured = persistedSnapshotState.get(this);
670
- if (captured !== void 0) {
671
- if (captured !== this.serializeState()) throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence; ingest a correcting snapshot instead.`);
672
- } else if (this.id && await this.isSaved()) throw new Error(`CampaignMetricSnapshot ${this.id}: refusing to overwrite an existing snapshot from a non-hydrated instance; snapshots are immutable evidence.`);
673
- else if (this.dedupeKey) try {
674
- const row = await this.db.get(this.tableName, { dedupe_key: this.dedupeKey });
675
- if (row && row.id !== this.id) throw new Error(`CampaignMetricSnapshot (dedupeKey '${this.dedupeKey}'): a snapshot with this dedupe key already exists; use MetricIngestionService for idempotent ingestion.`);
676
- } catch (error) {
677
- if (error instanceof Error && error.message.includes("idempotent ingestion")) throw error;
678
- }
679
- const result = await super.save();
680
- persistedSnapshotState.set(this, this.serializeState());
681
- return result;
682
- }
683
- async delete() {
684
- throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence and cannot be deleted.`);
685
- }
686
- assertMetrics() {
687
- if (Number.isNaN(this.periodStart.getTime()) || Number.isNaN(this.periodEnd.getTime())) throw new Error("CampaignMetricSnapshot periodStart and periodEnd must be valid dates.");
688
- if (this.periodEnd.getTime() < this.periodStart.getTime()) throw new Error("CampaignMetricSnapshot periodEnd must be on or after periodStart.");
689
- const integerMetrics = [
690
- ["spendCents", this.spendCents],
691
- ["impressions", this.impressions],
692
- ["clicks", this.clicks],
693
- ["conversions", this.conversions],
694
- ["leads", this.leads]
695
- ];
696
- for (const [name, value] of integerMetrics) if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer.`);
697
- if (this.revenueCents !== null && (!Number.isInteger(this.revenueCents) || this.revenueCents < 0)) throw new Error("revenueCents must be null or a non-negative integer.");
698
- }
699
- async assertChannelMatchesCampaign() {
700
- if (!this.campaignChannelId) return;
701
- if ((await this.loadRelated("campaignChannelId")).campaignId !== this.campaignId) throw new Error(`CampaignMetricSnapshot campaignChannelId '${this.campaignChannelId}' does not belong to campaignId '${this.campaignId}'.`);
702
- }
703
- serializeState() {
704
- return JSON.stringify({
705
- tenantId: this.tenantId,
706
- campaignId: this.campaignId,
707
- campaignChannelId: this.campaignChannelId,
708
- periodStart: this.periodStart.toISOString(),
709
- periodEnd: this.periodEnd.toISOString(),
710
- spendCents: this.spendCents,
711
- impressions: this.impressions,
712
- clicks: this.clicks,
713
- conversions: this.conversions,
714
- leads: this.leads,
715
- revenueCents: this.revenueCents,
716
- source: this.source,
717
- dedupeKey: this.dedupeKey
718
- });
719
- }
720
- static coerceDate(value) {
721
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
722
- if (typeof value === "number" || typeof value === "string") {
723
- const date = new Date(value);
724
- return Number.isNaN(date.getTime()) ? null : date;
725
- }
726
- return null;
727
- }
728
- };
729
- __decorateClass([tenantId({ nullable: true })], CampaignMetricSnapshot.prototype, "tenantId", 2);
730
- __decorateClass([foreignKey(Campaign, { required: true })], CampaignMetricSnapshot.prototype, "campaignId", 2);
731
- __decorateClass([foreignKey(CampaignChannel, { nullable: true })], CampaignMetricSnapshot.prototype, "campaignChannelId", 2);
732
- __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "periodStart", 2);
733
- __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "periodEnd", 2);
734
- __decorateClass([field({
735
- type: "integer",
736
- nullable: true
737
- })], CampaignMetricSnapshot.prototype, "revenueCents", 2);
738
- __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "source", 2);
739
- __decorateClass([field({ required: true })], CampaignMetricSnapshot.prototype, "dedupeKey", 2);
740
- CampaignMetricSnapshot = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
741
- tableName: "campaign_metric_snapshots",
742
- conflictColumns: ["dedupe_key"],
743
- api: { include: [
744
- "list",
745
- "get",
746
- "create"
747
- ] },
748
- mcp: { include: [
749
- "list",
750
- "get",
751
- "create"
752
- ] },
753
- cli: { include: [
754
- "list",
755
- "get",
756
- "create"
757
- ] }
758
- })], CampaignMetricSnapshot);
759
- //#endregion
760
- //#region src/collections/CampaignMetricSnapshotCollection.ts
761
- var CampaignMetricSnapshotCollection = class extends SmrtCollection {
762
- static _itemClass = CampaignMetricSnapshot;
763
- async findByDedupeKey(dedupeKey) {
764
- if (!dedupeKey) return null;
765
- return (await this.list({
766
- where: { dedupeKey },
767
- limit: 1
768
- }))[0] ?? null;
769
- }
770
- async getOrCreateByDedupeKey(options) {
771
- const dedupeKey = options.dedupeKey ?? "";
772
- if (!dedupeKey) throw new Error("CampaignMetricSnapshotCollection.getOrCreateByDedupeKey requires a dedupeKey.");
773
- const existing = await this.findByDedupeKey(dedupeKey);
774
- if (existing) return {
775
- snapshot: existing,
776
- created: false
777
- };
778
- try {
779
- return {
780
- snapshot: await this.create({
781
- ...options,
782
- periodStart: options.periodStart,
783
- periodEnd: options.periodEnd,
784
- _insertOnly: true
785
- }),
786
- created: true
787
- };
788
- } catch (error) {
789
- const concurrent = await this.findByDedupeKey(dedupeKey);
790
- if (concurrent) return {
791
- snapshot: concurrent,
792
- created: false
793
- };
794
- throw error;
795
- }
796
- }
797
- async findByCampaign(campaignId) {
798
- return await this.list({
799
- where: { campaignId },
800
- orderBy: "period_start ASC"
801
- });
802
- }
803
- async findByChannel(campaignChannelId) {
804
- return await this.list({
805
- where: { campaignChannelId },
806
- orderBy: "period_start ASC"
807
- });
808
- }
809
- };
810
- //#endregion
811
1051
  //#region src/services/BudgetPacingService.ts
812
1052
  var BudgetPacingService = class BudgetPacingService {
813
1053
  constructor(campaigns, channels, snapshots) {
@@ -827,15 +1067,11 @@ var BudgetPacingService = class BudgetPacingService {
827
1067
  const all = await this.snapshots.findByCampaign(campaignId);
828
1068
  const rollups = all.filter((snapshot) => !snapshot.campaignChannelId);
829
1069
  const used = this.selectCampaignEvidence(all);
830
- return this.calculate({
831
- campaign,
832
- snapshots: used,
833
- budgetCents: campaign.budgetCents,
834
- startAt: campaign.startAt,
835
- endAt: campaign.endAt,
836
- at,
1070
+ return calculateCampaignPacing(campaign, {
1071
+ spendCents: used.reduce((sum, snapshot) => sum + snapshot.spendCents, 0),
1072
+ snapshotCount: used.length,
837
1073
  usedCampaignRollups: rollups.length > 0
838
- });
1074
+ }, at);
839
1075
  }
840
1076
  async getChannelPacing(campaignChannelId, at = /* @__PURE__ */ new Date()) {
841
1077
  const channel = await this.channels.get({ id: campaignChannelId });
@@ -844,58 +1080,21 @@ var BudgetPacingService = class BudgetPacingService {
844
1080
  if (!campaign) throw new Error(`Campaign '${channel.campaignId}' was not found.`);
845
1081
  const snapshots = await this.snapshots.findByChannel(campaignChannelId);
846
1082
  return {
847
- ...this.calculate({
1083
+ ...calculatePacing({
848
1084
  campaign,
849
- snapshots,
1085
+ evidence: {
1086
+ spendCents: snapshots.reduce((sum, snapshot) => sum + snapshot.spendCents, 0),
1087
+ snapshotCount: snapshots.length,
1088
+ usedCampaignRollups: false
1089
+ },
850
1090
  budgetCents: channel.allocatedBudgetCents,
851
1091
  startAt: channel.startAt ?? campaign.startAt,
852
1092
  endAt: channel.endAt ?? campaign.endAt,
853
- at,
854
- usedCampaignRollups: false
855
- }),
856
- campaignChannelId
857
- };
858
- }
859
- calculate({ campaign, snapshots, budgetCents, startAt, endAt, at, usedCampaignRollups }) {
860
- const spendCents = snapshots.reduce((sum, snapshot) => sum + snapshot.spendCents, 0);
861
- const elapsedFraction = this.elapsedFraction(startAt, endAt, at);
862
- const expectedSpendCents = elapsedFraction === null ? null : Math.round(budgetCents * elapsedFraction);
863
- const varianceCents = expectedSpendCents === null ? null : spendCents - expectedSpendCents;
864
- const budgetFraction = budgetCents > 0 ? spendCents / budgetCents : null;
865
- return {
866
- campaignId: campaign.id ?? "",
867
- currency: campaign.currency,
868
- budgetCents,
869
- spendCents,
870
- remainingCents: budgetCents - spendCents,
871
- expectedSpendCents,
872
- varianceCents,
873
- budgetFraction,
874
- elapsedFraction,
875
- status: this.pacingStatus({
876
- campaign,
877
- budgetCents,
878
- spendCents,
879
- elapsedFraction,
880
- varianceCents,
881
- startAt,
882
1093
  at
883
1094
  }),
884
- snapshotCount: snapshots.length,
885
- usedCampaignRollups
1095
+ campaignChannelId
886
1096
  };
887
1097
  }
888
- pacingStatus({ campaign, budgetCents, spendCents, elapsedFraction, varianceCents, startAt, at }) {
889
- if (budgetCents <= 0) return "unbudgeted";
890
- if (spendCents > budgetCents) return "over_budget";
891
- if (startAt && at.getTime() < startAt.getTime()) return "not_started";
892
- if (campaign.status === "completed" || campaign.status === "archived" || elapsedFraction === 1) return "complete";
893
- if (varianceCents === null) return "on_track";
894
- const toleranceCents = Math.max(1, Math.round(budgetCents * .05));
895
- if (varianceCents > toleranceCents) return "ahead";
896
- if (varianceCents < -toleranceCents) return "behind";
897
- return "on_track";
898
- }
899
1098
  /**
900
1099
  * Prefer a campaign rollup for each exact reporting period, while retaining
901
1100
  * channel evidence for periods that do not yet have a rollup. This prevents
@@ -908,13 +1107,6 @@ var BudgetPacingService = class BudgetPacingService {
908
1107
  periodKey(snapshot) {
909
1108
  return `${snapshot.periodStart.toISOString()}:${snapshot.periodEnd.toISOString()}`;
910
1109
  }
911
- elapsedFraction(startAt, endAt, at) {
912
- if (!startAt || !endAt) return null;
913
- const start = startAt.getTime();
914
- const end = endAt.getTime();
915
- if (end <= start) return at.getTime() < start ? 0 : 1;
916
- return Math.min(1, Math.max(0, (at.getTime() - start) / (end - start)));
917
- }
918
1110
  };
919
1111
  //#endregion
920
1112
  //#region src/services/CampaignLifecycleService.ts