@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.
- package/AGENTS.md +5 -3
- package/README.md +23 -4
- package/dist/collections/CampaignCollection.d.ts +10 -1
- package/dist/collections/CampaignCollection.d.ts.map +1 -1
- package/dist/index.js +448 -256
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +25 -1
- package/dist/services/BudgetPacingService.d.ts +0 -3
- package/dist/services/BudgetPacingService.d.ts.map +1 -1
- package/dist/services/pacing-calculation.d.ts +18 -0
- package/dist/services/pacing-calculation.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +16 -5
- package/dist/svelte/components/BudgetPacing.svelte +3 -0
- package/dist/svelte/components/BudgetPacing.svelte.d.ts +3 -0
- package/dist/svelte/components/BudgetPacing.svelte.d.ts.map +1 -1
- package/dist/svelte/components/CampaignDetail.svelte +5 -0
- package/dist/svelte/components/CampaignDetail.svelte.d.ts +5 -0
- package/dist/svelte/components/CampaignDetail.svelte.d.ts.map +1 -1
- package/dist/svelte/components/CampaignList.svelte +4 -0
- package/dist/svelte/components/CampaignList.svelte.d.ts +4 -0
- package/dist/svelte/components/CampaignList.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ChannelMix.svelte +3 -0
- package/dist/svelte/components/ChannelMix.svelte.d.ts +3 -0
- package/dist/svelte/components/ChannelMix.svelte.d.ts.map +1 -1
- package/dist/svelte/components/MarketingDashboard.svelte +4 -0
- package/dist/svelte/components/MarketingDashboard.svelte.d.ts +4 -0
- package/dist/svelte/components/MarketingDashboard.svelte.d.ts.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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
|
-
|
|
422
|
-
|
|
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
|
|
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
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
...
|
|
1083
|
+
...calculatePacing({
|
|
848
1084
|
campaign,
|
|
849
|
-
|
|
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
|
-
|
|
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
|