@happyvertical/smrt-marketing 0.57.45 → 0.57.47

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 CHANGED
@@ -30,8 +30,12 @@ publish-surface changes.
30
30
  intentionally plain strings so ads, social, messages, content, events, and
31
31
  referral programs remain peer packages. Allocation and schedule overrides
32
32
  use integer cents and dates.
33
- - **CampaignMetricSnapshot**: immutable period evidence with a global
34
- `dedupe_key` natural key. A snapshot always carries `campaignId`; a null
33
+ - **CampaignMetricSnapshot**: immutable period evidence with a tenant-local
34
+ `(tenant_id, dedupe_key)` natural key (#3305 M2): the caller key is opaque,
35
+ so two tenants may file the same key. `findByDedupeKey()` /
36
+ `getOrCreateByDedupeKey()` and the save guard resolve one owner (explicit
37
+ `tenantId`, else the context; neither → global evidence only; a foreign id
38
+ under a context fails closed), so a system-context ingest names its tenant. A snapshot always carries `campaignId`; a null
35
39
  `campaignChannelId` means campaign-level rollup evidence; when present, the
36
40
  channel must belong to that same campaign. API/MCP/CLI expose create/list/get
37
41
  only, and the save guard rejects hydrated edits, blind-id overwrites, and
@@ -98,5 +102,15 @@ render time.
98
102
  both.
99
103
  - Corrections append a new metric snapshot with a new dedupe key; never edit
100
104
  persisted evidence.
105
+ - Key rollout (#3305 M2): `smrt db:migrate` adds
106
+ `campaign_metric_snapshots_tenant_id_dedupe_key_idx` and drops the superseded
107
+ `campaign_metric_snapshots_dedupe_key_idx`; rows are untouched. Deploy code
108
+ and migration together with writers drained (old code upserts against the
109
+ dropped arbiter); a revert needs collision analysis once tenants share keys.
110
+ Global (NULL-tenant) evidence is unique in the database only on PostgreSQL
111
+ 15+ (NULLS NOT DISTINCT); elsewhere the model's null-aware check enforces it,
112
+ so concurrent global ingests need PostgreSQL 15+ or a single writer.
113
+ - New snapshots must belong to their campaign's owner, and a tenant context
114
+ cannot file evidence for another tenant (checked before any dedupe lookup).
101
115
  - Table names are global: `campaigns`, `campaign_channels`, and
102
116
  `campaign_metric_snapshots`.
package/README.md CHANGED
@@ -50,7 +50,9 @@ await ingestion.ingest({
50
50
  conversions: 35,
51
51
  leads: 20,
52
52
  source: 'ad-platform',
53
- dedupeKey: `${tenantId}:summer-demand-2026:ad-group-42:2026-07-01`,
53
+ // Unique per tenant: (tenant_id, dedupe_key). A replay by the same owner
54
+ // returns the original snapshot unchanged.
55
+ dedupeKey: 'summer-demand-2026:ad-group-42:2026-07-01',
54
56
  });
55
57
 
56
58
  // Channel-scoped evidence is accepted only when the channel belongs to the
@@ -3,7 +3,23 @@ import { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';
3
3
  import type { CampaignMetricSnapshotOptions } from '../types.js';
4
4
  export declare class CampaignMetricSnapshotCollection extends SmrtCollection<CampaignMetricSnapshot> {
5
5
  static readonly _itemClass: typeof CampaignMetricSnapshot;
6
- findByDedupeKey(dedupeKey: string): Promise<CampaignMetricSnapshot | null>;
6
+ /**
7
+ * The tenant a dedupe key resolves in (#3305 M2): an explicit `tenantId`,
8
+ * else the active tenant context, else none (global evidence). This is the
9
+ * owner the tenancy interceptor gives a new row, so the replay lookup, the
10
+ * create and the race re-read agree. A system context carries no tenant,
11
+ * so an administrative ingest names its tenant.
12
+ */
13
+ private resolveOwner;
14
+ /**
15
+ * The snapshot filed under `dedupeKey` by one owner: `options.tenantId`,
16
+ * else the active context; without either, global evidence only. Keys are
17
+ * tenant-local, so another tenant's snapshot is never returned, and an
18
+ * active context cannot read another tenant's by passing its id.
19
+ */
20
+ findByDedupeKey(dedupeKey: string, options?: {
21
+ tenantId?: string | null;
22
+ }): Promise<CampaignMetricSnapshot | null>;
7
23
  getOrCreateByDedupeKey(options: CampaignMetricSnapshotOptions): Promise<{
8
24
  snapshot: CampaignMetricSnapshot;
9
25
  created: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"CampaignMetricSnapshotCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignMetricSnapshotCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAEjE,qBAAa,gCAAiC,SAAQ,cAAc,CAAC,sBAAsB,CAAC;IAC1F,MAAM,CAAC,QAAQ,CAAC,UAAU,gCAA0B;IAE9C,eAAe,CACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAMnC,sBAAsB,CAC1B,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC;QAAE,QAAQ,EAAE,sBAAsB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IA8B5D,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAOrE,aAAa,CACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,sBAAsB,EAAE,CAAC;CAMrC;AAED,eAAe,gCAAgC,CAAC"}
1
+ {"version":3,"file":"CampaignMetricSnapshotCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignMetricSnapshotCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAEjE,qBAAa,gCAAiC,SAAQ,cAAc,CAAC,sBAAsB,CAAC;IAC1F,MAAM,CAAC,QAAQ,CAAC,UAAU,gCAA0B;IAEpD;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAcpB;;;;;OAKG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAO,GACzC,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAoBnC,sBAAsB,CAC1B,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC;QAAE,QAAQ,EAAE,sBAAsB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAkC5D,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAOrE,aAAa,CACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,sBAAsB,EAAE,CAAC;CAMrC;AAED,eAAe,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { CAMPAIGN_CHANNEL_KINDS, CAMPAIGN_CHANNEL_STATUSES, CAMPAIGN_STATUSES }
2
2
  import { ObjectRegistry, SmrtCollection, SmrtObject, SmrtRecipe, 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.57.45","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","api":{"include":["list","get","create","update","delete"]},"mcp":{"include":["list","get","create","update"]},"cli":{"include":["list","get","create","update","delete"]}},"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 \\"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"},"importPath":"@happyvertical/smrt-marketing"},"@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,"unionBranches":[{"type":"string"},{"type":"SmrtWhereClause<Campaign>"}]},{"name":"options","type":"object","optional":true,"memberTypes":["CollectionCacheConfig | false"]}],"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, unknown>[]>","isStatic":false,"isPublic":true},"findByCampaignKey":{"name":"findByCampaignKey","async":true,"parameters":[{"name":"campaignKey","type":"string","optional":false},{"name":"tenantId","type":"string | null","optional":true,"unionBranches":[{"type":"string"},{"type":"null"}]}],"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"name":"customerIds","type":"string[]","optional":false}],"returnType":"Promise<CampaignCustomerSummary[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"campaigns","api":{"include":["list","get","create","update"]},"mcp":{"include":["list","get","create","update"]},"cli":{"include":["list","get","create","update"]}},"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 \\"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,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"},"@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","api":{"include":["list","get","create"]},"mcp":{"include":["list","get","create"]},"cli":{"include":["list","get","create"]}},"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 \\"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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The customer this campaign is run for, if any.","_meta":{"description":"The customer this campaign is run for, if any.","validate":true,"nullable":true}},"campaignKey":{"type":"text","required":true,"default":"","description":"A short reference of your own for this campaign, kept the same once it is in use.","_meta":{"required":true,"description":"A short reference of your own for this campaign, kept the same once it is in use."}},"name":{"type":"text","required":true,"default":"","description":"What the campaign is called.","_meta":{"required":true,"description":"What the campaign is called."}},"objective":{"type":"text","required":false,"default":"","description":"What the campaign is meant to achieve, such as awareness or new leads.","_meta":{"description":"What the campaign is meant to achieve, such as awareness or new leads."}},"status":{"type":"text","required":false,"default":"draft","description":"Where the campaign stands, from draft through active to completed and archived.","_meta":{"description":"Where the campaign stands, from draft through active to completed and archived."}},"startAt":{"type":"datetime","required":false,"description":"When the campaign starts.","_meta":{"description":"When the campaign starts."}},"endAt":{"type":"datetime","required":false,"description":"When the campaign ends.","_meta":{"description":"When the campaign ends."}},"budgetCents":{"type":"integer","required":false,"default":0,"description":"The total you plan to spend on the campaign, in cents.","_meta":{"description":"The total you plan to spend on the campaign, in cents."}},"currency":{"type":"text","required":false,"default":"USD","description":"The currency every amount on this campaign is in.","_meta":{"description":"The currency every amount on this campaign is in."}},"metadata":{"type":"text","required":false,"default":"{}"}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"transitionTo":{"name":"transitionTo","async":false,"parameters":[{"name":"next","type":"CampaignStatus","optional":false}],"returnType":"this","isStatic":false,"isPublic":true},"getMetadata":{"name":"getMetadata","async":false,"parameters":[],"returnType":"Record<string, unknown>","isStatic":false,"isPublic":true},"setMetadata":{"name":"setMetadata","async":false,"parameters":[{"name":"metadata","type":"Record<string, unknown>","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","displayLabelField":"name","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,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The campaign this channel runs for.","_meta":{"description":"The campaign this channel runs for.","required":true}},"channelKind":{"type":"text","required":true,"default":"","description":"What sort of channel this is, such as an ad group, a social post, a message, content or an event.","_meta":{"required":true,"description":"What sort of channel this is, such as an ad group, a social post, a message, content or an event."}},"channelRef":{"type":"text","required":true,"default":"","description":"The name or reference the channel goes by, so its results can be matched to it.","_meta":{"required":true,"description":"The name or reference the channel goes by, so its results can be matched to it."}},"allocatedBudgetCents":{"type":"integer","required":false,"default":0,"description":"The share of the campaign budget set aside for this channel, in cents.","_meta":{"description":"The share of the campaign budget set aside for this channel, in cents."}},"startAt":{"type":"datetime","required":false,"description":"When this channel starts, if different from the campaign.","_meta":{"description":"When this channel starts, if different from the campaign."}},"endAt":{"type":"datetime","required":false,"description":"When this channel ends, if different from the campaign.","_meta":{"description":"When this channel ends, if different from the campaign."}},"status":{"type":"text","required":false,"default":"planned","description":"Where this channel stands, from planned through active to completed or cancelled.","_meta":{"description":"Where this channel stands, from planned through active to completed or cancelled."}}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The campaign these results belong to.","_meta":{"description":"The campaign these results belong to.","required":true}},"campaignChannelId":{"type":"foreignKey","required":false,"related":"CampaignChannel","description":"The channel these results came from. Empty when they cover the whole campaign.","_meta":{"description":"The channel these results came from. Empty when they cover the whole campaign.","nullable":true}},"periodStart":{"type":"datetime","required":true,"description":"The first day of the period these results cover.","_meta":{"required":true,"description":"The first day of the period these results cover."}},"periodEnd":{"type":"datetime","required":true,"description":"The last day of the period these results cover.","_meta":{"required":true,"description":"The last day of the period these results cover."}},"spendCents":{"type":"integer","required":false,"default":0,"description":"What was spent in the period, in cents.","_meta":{"description":"What was spent in the period, in cents."}},"impressions":{"type":"integer","required":false,"default":0,"description":"How many times the campaign was shown.","_meta":{"description":"How many times the campaign was shown."}},"clicks":{"type":"integer","required":false,"default":0,"description":"How many times someone clicked through.","_meta":{"description":"How many times someone clicked through."}},"conversions":{"type":"integer","required":false,"default":0,"description":"How many people did what the campaign asked, such as buying.","_meta":{"description":"How many people did what the campaign asked, such as buying."}},"leads":{"type":"integer","required":false,"default":0,"description":"How many new leads came in.","_meta":{"description":"How many new leads came in."}},"revenueCents":{"type":"integer","required":false,"description":"The sales brought in during the period, in cents, if known.","_meta":{"nullable":true,"description":"The sales brought in during the period, in cents, if known."}},"source":{"type":"text","required":true,"default":"","description":"Where these results were reported from.","_meta":{"required":true,"description":"Where these results were reported from."}},"dedupeKey":{"type":"text","required":true,"default":"","description":"The reference this result is filed under, so the same report is never counted twice.","_meta":{"required":true,"description":"The reference this result is filed under, so the same report is never counted twice."}}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","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"},"importPath":"@happyvertical/smrt-marketing"}},"moduleType":"smrt","recipes":[{"id":"marketing.campaigns","className":"MarketingCampaignsRecipe","label":"Campaigns","summary":"Plan a marketing campaign, set its budget and dates, choose where it runs and see what it brought in.","synonyms":["marketing","marketing campaigns","promotions","advertising","ad campaigns","campaign results"],"models":["@happyvertical/smrt-marketing:Campaign","@happyvertical/smrt-marketing:CampaignChannel","@happyvertical/smrt-marketing:CampaignMetricSnapshot"],"nav":[{"label":"Campaigns","model":"@happyvertical/smrt-marketing:Campaign","icon":"tag","description":"Every campaign, with its budget, dates and where it stands.","noun":"campaign"},{"label":"Channels","model":"@happyvertical/smrt-marketing:CampaignChannel","icon":"layers","description":"Where each campaign runs, such as an ad group or a social post, and its share of the budget.","noun":"channel"}],"requires":[],"section":{"id":"grow","label":"Grow","icon":"tag","description":"Bring in new customers and reward the people who help."},"options":{"@happyvertical/smrt-marketing:Campaign":{"fields":{"name":{"order":1},"objective":{"order":2},"status":{"order":3},"startAt":{"label":"Starts","order":4},"endAt":{"label":"Ends","order":5},"budgetCents":{"label":"Budget (cents)","order":6},"currency":{"order":7},"customerId":{"label":"Customer","visibility":"advanced"},"campaignKey":{"label":"Reference","visibility":"advanced"},"tenantId":{"visibility":"hidden"},"metadata":{"visibility":"hidden"}}},"@happyvertical/smrt-marketing:CampaignChannel":{"fields":{"campaignId":{"label":"Campaign","order":1},"channelKind":{"label":"Kind","order":2},"channelRef":{"label":"Name","order":3},"allocatedBudgetCents":{"label":"Budget share (cents)","order":4},"status":{"order":5},"startAt":{"label":"Starts","order":6},"endAt":{"label":"Ends","order":7},"tenantId":{"visibility":"hidden"}}},"@happyvertical/smrt-marketing:CampaignMetricSnapshot":{"fields":{"campaignId":{"label":"Campaign","order":1},"campaignChannelId":{"label":"Channel","order":2},"periodStart":{"label":"From","order":3},"periodEnd":{"label":"To","order":4},"spendCents":{"label":"Spend (cents)","order":5},"impressions":{"order":6},"clicks":{"order":7},"conversions":{"order":8},"leads":{"order":9},"revenueCents":{"label":"Revenue (cents)","order":10},"source":{"order":11},"tenantId":{"visibility":"hidden"},"dedupeKey":{"visibility":"hidden"}}}},"help":{"markdown":"## Overview\\n\\nCampaigns is where you plan a push to bring in customers: a sale, a launch, a\\nseasonal promotion. Each campaign has a budget, start and end dates and a\\nstatus that shows where it stands, from draft through active to completed. A\\ncampaign runs on one or more channels, such as an ad group, a social post, an\\nemail or an event, each with its share of the budget. Results come back from\\nthe channels' reporting, period by period, so you can see what each campaign\\nand channel brought in for what it cost.\\n\\n## Tasks\\n\\n### Plan a campaign\\n\\n1. Open the list of campaigns and choose **New**.\\n2. Type the **{field:Campaign.name}**, for example \\"Spring widget sale\\".\\n3. Say what it is for in **{field:Campaign.objective}**, such as awareness or new leads.\\n4. Set when it **{field:Campaign.startAt}** and when it **{field:Campaign.endAt}**. The end cannot be before the start.\\n5. Enter the **{field:Campaign.budgetCents}** as a whole number of cents, so $500 is 50000, and check the **{field:Campaign.currency}**.\\n6. Save. A new campaign starts as a draft.\\n\\n### See where a campaign stands\\n\\n1. Open the list of campaigns.\\n2. The **{field:Campaign.status}** shows whether each one is still a draft, scheduled, active, paused, completed or archived. It moves forward as the campaign is scheduled, run and wrapped up; you do not set it by hand.\\n3. Open a campaign to change its name, dates or budget while it is still being planned.\\n\\n### Add a channel to a campaign\\n\\n1. Open the list of channels and choose **New**.\\n2. Pick the **{field:CampaignChannel.campaignId}** it belongs to.\\n3. Choose the **{field:CampaignChannel.channelKind}**, such as an ad group, a social post, a message, content or an event.\\n4. Type the **{field:CampaignChannel.channelRef}** the channel goes by, so its results can be matched to it later.\\n5. Enter its **{field:CampaignChannel.allocatedBudgetCents}** as a whole number of cents.\\n6. If the channel runs on different dates from the campaign, set when it **{field:CampaignChannel.startAt}** and **{field:CampaignChannel.endAt}**.\\n7. Save. You can edit a channel later, or remove one that will not run.\\n\\n### Read the results\\n\\nResults arrive from the channels' reporting; nobody types them in. Each one\\ncovers a period for one campaign, or for one channel within it. A result is\\nnever changed once it is in: a correction arrives as a new result for the same\\nperiod.\\n\\n1. Open a campaign and look at its results, listed under the campaign.\\n2. Each result names the **{field:CampaignMetricSnapshot.campaignId}** it belongs to. A result with no **{field:CampaignMetricSnapshot.campaignChannelId}** covers the whole campaign.\\n3. Check the period, **{field:CampaignMetricSnapshot.periodStart}** to **{field:CampaignMetricSnapshot.periodEnd}**.\\n4. Compare the **{field:CampaignMetricSnapshot.spendCents}** with the **{field:CampaignMetricSnapshot.clicks}**, **{field:CampaignMetricSnapshot.leads}** and **{field:CampaignMetricSnapshot.conversions}** it brought in, and the **{field:CampaignMetricSnapshot.revenueCents}** where the channel reports it.\\n5. The **{field:CampaignMetricSnapshot.source}** says which channel's reporting sent the result.\\n","fieldRefs":["Campaign.budgetCents","Campaign.currency","Campaign.endAt","Campaign.name","Campaign.objective","Campaign.startAt","Campaign.status","CampaignChannel.allocatedBudgetCents","CampaignChannel.campaignId","CampaignChannel.channelKind","CampaignChannel.channelRef","CampaignChannel.endAt","CampaignChannel.startAt","CampaignMetricSnapshot.campaignChannelId","CampaignMetricSnapshot.campaignId","CampaignMetricSnapshot.clicks","CampaignMetricSnapshot.conversions","CampaignMetricSnapshot.leads","CampaignMetricSnapshot.periodEnd","CampaignMetricSnapshot.periodStart","CampaignMetricSnapshot.revenueCents","CampaignMetricSnapshot.source","CampaignMetricSnapshot.spendCents"]},"demo":{"mode":"server","reasons":["Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."]}}],"smrtDependencies":["@happyvertical/smrt-commerce","@happyvertical/smrt-core","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3624"],"via":["@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
5
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-marketing","packageVersion":"0.57.47","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","api":{"include":["list","get","create","update","delete"]},"mcp":{"include":["list","get","create","update"]},"cli":{"include":["list","get","create","update","delete"]}},"extends":"SmrtCollection","extendsTypeArg":"CampaignChannel","exportName":"CampaignChannelCollection","collectionExportName":"CampaignChannelCollectionCollection","collectionOf":"@happyvertical/smrt-marketing:CampaignChannel","inheritedExposure":["api","mcp","cli"],"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"},"importPath":"@happyvertical/smrt-marketing"},"@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,"unionBranches":[{"type":"string"},{"type":"SmrtWhereClause<Campaign>"}]},{"name":"options","type":"object","optional":true,"memberTypes":["CollectionCacheConfig | false"]}],"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, unknown>[]>","isStatic":false,"isPublic":true},"findByCampaignKey":{"name":"findByCampaignKey","async":true,"parameters":[{"name":"campaignKey","type":"string","optional":false},{"name":"tenantId","type":"string | null","optional":true,"unionBranches":[{"type":"string"},{"type":"null"}]}],"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"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,"unionBranches":[{"type":"string"},{"type":"null"}]},{"name":"customerIds","type":"string[]","optional":false}],"returnType":"Promise<CampaignCustomerSummary[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"campaigns","api":{"include":["list","get","create","update"]},"mcp":{"include":["list","get","create","update"]},"cli":{"include":["list","get","create","update"]}},"extends":"SmrtCollection","extendsTypeArg":"Campaign","exportName":"CampaignCollection","collectionExportName":"CampaignCollectionCollection","collectionOf":"@happyvertical/smrt-marketing:Campaign","inheritedExposure":["api","mcp","cli"],"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,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"},"@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},{"name":"options","type":"object","optional":true,"memberTypes":["string | null"]}],"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","api":{"include":["list","get","create"]},"mcp":{"include":["list","get","create"]},"cli":{"include":["list","get","create"]}},"extends":"SmrtCollection","extendsTypeArg":"CampaignMetricSnapshot","exportName":"CampaignMetricSnapshotCollection","collectionExportName":"CampaignMetricSnapshotCollectionCollection","collectionOf":"@happyvertical/smrt-marketing:CampaignMetricSnapshot","inheritedExposure":["api","mcp","cli"],"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_tenant_id_dedupe_key_idx","columns":["tenant_id","dedupe_key"],"unique":true,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The customer this campaign is run for, if any.","_meta":{"description":"The customer this campaign is run for, if any.","validate":true,"nullable":true}},"campaignKey":{"type":"text","required":true,"default":"","description":"A short reference of your own for this campaign, kept the same once it is in use.","_meta":{"required":true,"description":"A short reference of your own for this campaign, kept the same once it is in use."}},"name":{"type":"text","required":true,"default":"","description":"What the campaign is called.","_meta":{"required":true,"description":"What the campaign is called."}},"objective":{"type":"text","required":false,"default":"","description":"What the campaign is meant to achieve, such as awareness or new leads.","_meta":{"description":"What the campaign is meant to achieve, such as awareness or new leads."}},"status":{"type":"text","required":false,"default":"draft","description":"Where the campaign stands, from draft through active to completed and archived.","_meta":{"description":"Where the campaign stands, from draft through active to completed and archived."}},"startAt":{"type":"datetime","required":false,"description":"When the campaign starts.","_meta":{"description":"When the campaign starts."}},"endAt":{"type":"datetime","required":false,"description":"When the campaign ends.","_meta":{"description":"When the campaign ends."}},"budgetCents":{"type":"integer","required":false,"default":0,"description":"The total you plan to spend on the campaign, in cents.","_meta":{"description":"The total you plan to spend on the campaign, in cents."}},"currency":{"type":"text","required":false,"default":"USD","description":"The currency every amount on this campaign is in.","_meta":{"description":"The currency every amount on this campaign is in."}},"metadata":{"type":"text","required":false,"default":"{}"}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"transitionTo":{"name":"transitionTo","async":false,"parameters":[{"name":"next","type":"CampaignStatus","optional":false}],"returnType":"this","isStatic":false,"isPublic":true},"getMetadata":{"name":"getMetadata","async":false,"parameters":[],"returnType":"Record<string, unknown>","isStatic":false,"isPublic":true},"setMetadata":{"name":"setMetadata","async":false,"parameters":[{"name":"metadata","type":"Record<string, unknown>","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","displayLabelField":"name","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,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The campaign this channel runs for.","_meta":{"description":"The campaign this channel runs for.","required":true}},"channelKind":{"type":"text","required":true,"default":"","description":"What sort of channel this is, such as an ad group, a social post, a message, content or an event.","_meta":{"required":true,"description":"What sort of channel this is, such as an ad group, a social post, a message, content or an event."}},"channelRef":{"type":"text","required":true,"default":"","description":"The name or reference the channel goes by, so its results can be matched to it.","_meta":{"required":true,"description":"The name or reference the channel goes by, so its results can be matched to it."}},"allocatedBudgetCents":{"type":"integer","required":false,"default":0,"description":"The share of the campaign budget set aside for this channel, in cents.","_meta":{"description":"The share of the campaign budget set aside for this channel, in cents."}},"startAt":{"type":"datetime","required":false,"description":"When this channel starts, if different from the campaign.","_meta":{"description":"When this channel starts, if different from the campaign."}},"endAt":{"type":"datetime","required":false,"description":"When this channel ends, if different from the campaign.","_meta":{"description":"When this channel ends, if different from the campaign."}},"status":{"type":"text","required":false,"default":"planned","description":"Where this channel stands, from planned through active to completed or cancelled.","_meta":{"description":"Where this channel stands, from planned through active to completed or cancelled."}}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","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"},"importPath":"@happyvertical/smrt-marketing"},"@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","description":"The campaign these results belong to.","_meta":{"description":"The campaign these results belong to.","required":true}},"campaignChannelId":{"type":"foreignKey","required":false,"related":"CampaignChannel","description":"The channel these results came from. Empty when they cover the whole campaign.","_meta":{"description":"The channel these results came from. Empty when they cover the whole campaign.","nullable":true}},"periodStart":{"type":"datetime","required":true,"description":"The first day of the period these results cover.","_meta":{"required":true,"description":"The first day of the period these results cover."}},"periodEnd":{"type":"datetime","required":true,"description":"The last day of the period these results cover.","_meta":{"required":true,"description":"The last day of the period these results cover."}},"spendCents":{"type":"integer","required":false,"default":0,"description":"What was spent in the period, in cents.","_meta":{"description":"What was spent in the period, in cents."}},"impressions":{"type":"integer","required":false,"default":0,"description":"How many times the campaign was shown.","_meta":{"description":"How many times the campaign was shown."}},"clicks":{"type":"integer","required":false,"default":0,"description":"How many times someone clicked through.","_meta":{"description":"How many times someone clicked through."}},"conversions":{"type":"integer","required":false,"default":0,"description":"How many people did what the campaign asked, such as buying.","_meta":{"description":"How many people did what the campaign asked, such as buying."}},"leads":{"type":"integer","required":false,"default":0,"description":"How many new leads came in.","_meta":{"description":"How many new leads came in."}},"revenueCents":{"type":"integer","required":false,"description":"The sales brought in during the period, in cents, if known.","_meta":{"nullable":true,"description":"The sales brought in during the period, in cents, if known."}},"source":{"type":"text","required":true,"default":"","description":"Where these results were reported from.","_meta":{"required":true,"description":"Where these results were reported from."}},"dedupeKey":{"type":"text","required":true,"default":"","description":"The reference this result is filed under, so the same report is never counted twice.","_meta":{"required":true,"description":"The reference this result is filed under, so the same report is never counted twice."}}},"methods":{"initialize":{"name":"initialize","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"campaign_metric_snapshots","conflictColumns":["tenant_id","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_tenant_id_dedupe_key_idx","columns":["tenant_id","dedupe_key"],"unique":true,"nullsNotDistinct":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"},"importPath":"@happyvertical/smrt-marketing"}},"moduleType":"smrt","recipes":[{"id":"marketing.campaigns","className":"MarketingCampaignsRecipe","label":"Campaigns","summary":"Plan a marketing campaign, set its budget and dates, choose where it runs and see what it brought in.","synonyms":["marketing","marketing campaigns","promotions","advertising","ad campaigns","campaign results"],"models":["@happyvertical/smrt-marketing:Campaign","@happyvertical/smrt-marketing:CampaignChannel","@happyvertical/smrt-marketing:CampaignMetricSnapshot"],"nav":[{"label":"Campaigns","model":"@happyvertical/smrt-marketing:Campaign","icon":"tag","description":"Every campaign, with its budget, dates and where it stands.","noun":"campaign"},{"label":"Channels","model":"@happyvertical/smrt-marketing:CampaignChannel","icon":"layers","description":"Where each campaign runs, such as an ad group or a social post, and its share of the budget.","noun":"channel"}],"requires":[],"section":{"id":"grow","label":"Grow","icon":"tag","description":"Bring in new customers and reward the people who help."},"options":{"@happyvertical/smrt-marketing:Campaign":{"fields":{"name":{"order":1},"objective":{"order":2},"status":{"order":3},"startAt":{"label":"Starts","order":4},"endAt":{"label":"Ends","order":5},"budgetCents":{"label":"Budget (cents)","order":6},"currency":{"order":7},"customerId":{"label":"Customer","visibility":"advanced"},"campaignKey":{"label":"Reference","visibility":"advanced"},"tenantId":{"visibility":"hidden"},"metadata":{"visibility":"hidden"}}},"@happyvertical/smrt-marketing:CampaignChannel":{"fields":{"campaignId":{"label":"Campaign","order":1},"channelKind":{"label":"Kind","order":2},"channelRef":{"label":"Name","order":3},"allocatedBudgetCents":{"label":"Budget share (cents)","order":4},"status":{"order":5},"startAt":{"label":"Starts","order":6},"endAt":{"label":"Ends","order":7},"tenantId":{"visibility":"hidden"}}},"@happyvertical/smrt-marketing:CampaignMetricSnapshot":{"fields":{"campaignId":{"label":"Campaign","order":1},"campaignChannelId":{"label":"Channel","order":2},"periodStart":{"label":"From","order":3},"periodEnd":{"label":"To","order":4},"spendCents":{"label":"Spend (cents)","order":5},"impressions":{"order":6},"clicks":{"order":7},"conversions":{"order":8},"leads":{"order":9},"revenueCents":{"label":"Revenue (cents)","order":10},"source":{"order":11},"tenantId":{"visibility":"hidden"},"dedupeKey":{"visibility":"hidden"}}}},"help":{"markdown":"## Overview\\n\\nCampaigns is where you plan a push to bring in customers: a sale, a launch, a\\nseasonal promotion. Each campaign has a budget, start and end dates and a\\nstatus that shows where it stands, from draft through active to completed. A\\ncampaign runs on one or more channels, such as an ad group, a social post, an\\nemail or an event, each with its share of the budget. Results come back from\\nthe channels' reporting, period by period, so you can see what each campaign\\nand channel brought in for what it cost.\\n\\n## Tasks\\n\\n### Plan a campaign\\n\\n1. Open the list of campaigns and choose **New**.\\n2. Type the **{field:Campaign.name}**, for example \\"Spring widget sale\\".\\n3. Say what it is for in **{field:Campaign.objective}**, such as awareness or new leads.\\n4. Set when it **{field:Campaign.startAt}** and when it **{field:Campaign.endAt}**. The end cannot be before the start.\\n5. Enter the **{field:Campaign.budgetCents}** as a whole number of cents, so $500 is 50000, and check the **{field:Campaign.currency}**.\\n6. Save. A new campaign starts as a draft.\\n\\n### See where a campaign stands\\n\\n1. Open the list of campaigns.\\n2. The **{field:Campaign.status}** shows whether each one is still a draft, scheduled, active, paused, completed or archived. It moves forward as the campaign is scheduled, run and wrapped up; you do not set it by hand.\\n3. Open a campaign to change its name, dates or budget while it is still being planned.\\n\\n### Add a channel to a campaign\\n\\n1. Open the list of channels and choose **New**.\\n2. Pick the **{field:CampaignChannel.campaignId}** it belongs to.\\n3. Choose the **{field:CampaignChannel.channelKind}**, such as an ad group, a social post, a message, content or an event.\\n4. Type the **{field:CampaignChannel.channelRef}** the channel goes by, so its results can be matched to it later.\\n5. Enter its **{field:CampaignChannel.allocatedBudgetCents}** as a whole number of cents.\\n6. If the channel runs on different dates from the campaign, set when it **{field:CampaignChannel.startAt}** and **{field:CampaignChannel.endAt}**.\\n7. Save. You can edit a channel later, or remove one that will not run.\\n\\n### Read the results\\n\\nResults arrive from the channels' reporting; nobody types them in. Each one\\ncovers a period for one campaign, or for one channel within it. A result is\\nnever changed once it is in: a correction arrives as a new result for the same\\nperiod.\\n\\n1. Open a campaign and look at its results, listed under the campaign.\\n2. Each result names the **{field:CampaignMetricSnapshot.campaignId}** it belongs to. A result with no **{field:CampaignMetricSnapshot.campaignChannelId}** covers the whole campaign.\\n3. Check the period, **{field:CampaignMetricSnapshot.periodStart}** to **{field:CampaignMetricSnapshot.periodEnd}**.\\n4. Compare the **{field:CampaignMetricSnapshot.spendCents}** with the **{field:CampaignMetricSnapshot.clicks}**, **{field:CampaignMetricSnapshot.leads}** and **{field:CampaignMetricSnapshot.conversions}** it brought in, and the **{field:CampaignMetricSnapshot.revenueCents}** where the channel reports it.\\n5. The **{field:CampaignMetricSnapshot.source}** says which channel's reporting sent the result.\\n","fieldRefs":["Campaign.budgetCents","Campaign.currency","Campaign.endAt","Campaign.name","Campaign.objective","Campaign.startAt","Campaign.status","CampaignChannel.allocatedBudgetCents","CampaignChannel.campaignId","CampaignChannel.channelKind","CampaignChannel.channelRef","CampaignChannel.endAt","CampaignChannel.startAt","CampaignMetricSnapshot.campaignChannelId","CampaignMetricSnapshot.campaignId","CampaignMetricSnapshot.clicks","CampaignMetricSnapshot.conversions","CampaignMetricSnapshot.leads","CampaignMetricSnapshot.periodEnd","CampaignMetricSnapshot.periodStart","CampaignMetricSnapshot.revenueCents","CampaignMetricSnapshot.source","CampaignMetricSnapshot.spendCents"]},"demo":{"mode":"server","reasons":["Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."]}}],"smrtDependencies":["@happyvertical/smrt-commerce","@happyvertical/smrt-core","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3624"],"via":["@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
6
6
  //#endregion
7
7
  //#region src/errors.ts
8
8
  var CampaignCustomerScopeError = class extends Error {
@@ -521,13 +521,18 @@ var CampaignMetricSnapshot = class extends SmrtObject {
521
521
  }
522
522
  async save(options) {
523
523
  this.assertMetrics();
524
+ this.assertOwnerInContext();
524
525
  await this.assertChannelMatchesCampaign();
525
526
  const captured = persistedSnapshotState.get(this);
526
527
  if (captured !== void 0) {
527
528
  if (captured !== this.serializeState()) throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence; ingest a correcting snapshot instead.`);
528
529
  } 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.`);
529
- else if (this.dedupeKey) try {
530
- const row = await this.db.get(this.tableName, { dedupe_key: this.dedupeKey });
530
+ else await this.assertCampaignOwnedByOwner();
531
+ if (captured === void 0 && this.dedupeKey) try {
532
+ const row = await this.db.get(this.tableName, {
533
+ tenant_id: this.ownerTenantId(),
534
+ dedupe_key: this.dedupeKey
535
+ });
531
536
  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.`);
532
537
  } catch (error) {
533
538
  if (error instanceof Error && error.message.includes("idempotent ingestion")) throw error;
@@ -539,6 +544,34 @@ var CampaignMetricSnapshot = class extends SmrtObject {
539
544
  async delete() {
540
545
  throw new Error(`CampaignMetricSnapshot ${this.id ?? "<new>"}: metric snapshots are immutable evidence and cannot be deleted.`);
541
546
  }
547
+ /** This row's owner: its tenant, else (before auto-populate) the context's. */
548
+ ownerTenantId() {
549
+ return this.tenantId || getCurrentTenant()?.tenantId || null;
550
+ }
551
+ /**
552
+ * A tenant context may only write its own evidence (#3305 M2). Checked
553
+ * before any raw lookup so the dedupe guard cannot reveal whether another
554
+ * tenant filed a key. The class does not opt into super-admin bypass.
555
+ */
556
+ assertOwnerInContext() {
557
+ const context = getCurrentTenant()?.tenantId;
558
+ if (context && this.tenantId && this.tenantId.toLowerCase() !== context.toLowerCase()) throw new TenantIsolationError(`Tenant isolation violation in CampaignMetricSnapshot: context tenant '${context}' cannot file evidence for tenant '${this.tenantId}'.`, {
559
+ tenantId: context,
560
+ attemptedTenantId: this.tenantId
561
+ });
562
+ }
563
+ /**
564
+ * New evidence belongs to its campaign's owner (#3305 M2): a snapshot is
565
+ * never filed globally, or for another tenant, against a tenant's campaign,
566
+ * so an unnamed administrative ingest cannot fork a tenant's evidence.
567
+ */
568
+ async assertCampaignOwnedByOwner() {
569
+ if (!this.campaignId) return;
570
+ const campaign = await this.db.get("campaigns", { id: this.campaignId });
571
+ const campaignOwner = campaign?.tenant_id ? String(campaign.tenant_id).toLowerCase() : null;
572
+ const owner = this.ownerTenantId()?.toLowerCase() ?? null;
573
+ if (!campaign || campaignOwner !== owner) throw new Error(`CampaignMetricSnapshot campaignId '${this.campaignId}' is not a campaign of this snapshot's owner.`);
574
+ }
542
575
  assertMetrics() {
543
576
  if (Number.isNaN(this.periodStart.getTime()) || Number.isNaN(this.periodEnd.getTime())) throw new Error("CampaignMetricSnapshot periodStart and periodEnd must be valid dates.");
544
577
  if (this.periodEnd.getTime() < this.periodStart.getTime()) throw new Error("CampaignMetricSnapshot periodEnd must be on or after periodStart.");
@@ -614,7 +647,7 @@ __decorateClass([field({
614
647
  })], CampaignMetricSnapshot.prototype, "dedupeKey", 2);
615
648
  CampaignMetricSnapshot = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
616
649
  tableName: "campaign_metric_snapshots",
617
- conflictColumns: ["dedupe_key"],
650
+ conflictColumns: ["tenant_id", "dedupe_key"],
618
651
  api: { include: [
619
652
  "list",
620
653
  "get",
@@ -635,17 +668,40 @@ CampaignMetricSnapshot = __decorateClass([TenantScoped({ mode: "optional" }), sm
635
668
  //#region src/collections/CampaignMetricSnapshotCollection.ts
636
669
  var CampaignMetricSnapshotCollection = class extends SmrtCollection {
637
670
  static _itemClass = CampaignMetricSnapshot;
638
- async findByDedupeKey(dedupeKey) {
671
+ /**
672
+ * The tenant a dedupe key resolves in (#3305 M2): an explicit `tenantId`,
673
+ * else the active tenant context, else none (global evidence). This is the
674
+ * owner the tenancy interceptor gives a new row, so the replay lookup, the
675
+ * create and the race re-read agree. A system context carries no tenant,
676
+ * so an administrative ingest names its tenant.
677
+ */
678
+ resolveOwner(tenantId) {
679
+ const context = getCurrentTenant()?.tenantId;
680
+ const owner = tenantId || context || null;
681
+ if (owner && context && owner.toLowerCase() !== context.toLowerCase()) throw new TenantIsolationError(`Tenant isolation violation in CampaignMetricSnapshotCollection: context tenant is '${context}' but the lookup requested '${owner}'`, {
682
+ tenantId: context,
683
+ attemptedTenantId: owner
684
+ });
685
+ return owner;
686
+ }
687
+ /**
688
+ * The snapshot filed under `dedupeKey` by one owner: `options.tenantId`,
689
+ * else the active context; without either, global evidence only. Keys are
690
+ * tenant-local, so another tenant's snapshot is never returned, and an
691
+ * active context cannot read another tenant's by passing its id.
692
+ */
693
+ async findByDedupeKey(dedupeKey, options = {}) {
639
694
  if (!dedupeKey) return null;
640
- return (await this.list({
641
- where: { dedupeKey },
642
- limit: 1
643
- }))[0] ?? null;
695
+ const tenantId = this.resolveOwner(options.tenantId);
696
+ const rows = await this.query(tenantId ? `SELECT * FROM ${this.tableName} WHERE dedupe_key = ? AND tenant_id = ? ORDER BY created_at, id` : `SELECT * FROM ${this.tableName} WHERE dedupe_key = ? AND tenant_id IS NULL ORDER BY created_at, id`, tenantId ? [dedupeKey, tenantId] : [dedupeKey], { allowRawOnTenantScoped: true });
697
+ const wanted = tenantId?.toLowerCase() ?? null;
698
+ return rows.find((row) => wanted ? String(row.tenantId ?? "").toLowerCase() === wanted : !row.tenantId) ?? null;
644
699
  }
645
700
  async getOrCreateByDedupeKey(options) {
646
701
  const dedupeKey = options.dedupeKey ?? "";
647
702
  if (!dedupeKey) throw new Error("CampaignMetricSnapshotCollection.getOrCreateByDedupeKey requires a dedupeKey.");
648
- const existing = await this.findByDedupeKey(dedupeKey);
703
+ const tenantId = this.resolveOwner(options.tenantId);
704
+ const existing = await this.findByDedupeKey(dedupeKey, { tenantId });
649
705
  if (existing) return {
650
706
  snapshot: existing,
651
707
  created: false
@@ -654,6 +710,7 @@ var CampaignMetricSnapshotCollection = class extends SmrtCollection {
654
710
  return {
655
711
  snapshot: await this.create({
656
712
  ...options,
713
+ tenantId,
657
714
  periodStart: options.periodStart,
658
715
  periodEnd: options.periodEnd,
659
716
  _insertOnly: true
@@ -661,7 +718,7 @@ var CampaignMetricSnapshotCollection = class extends SmrtCollection {
661
718
  created: true
662
719
  };
663
720
  } catch (error) {
664
- const concurrent = await this.findByDedupeKey(dedupeKey);
721
+ const concurrent = await this.findByDedupeKey(dedupeKey, { tenantId });
665
722
  if (concurrent) return {
666
723
  snapshot: concurrent,
667
724
  created: false