@happyvertical/smrt-marketing 0.57.46 → 0.57.48
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 +16 -2
- package/README.md +3 -1
- package/dist/collections/CampaignMetricSnapshotCollection.d.ts +17 -1
- package/dist/collections/CampaignMetricSnapshotCollection.d.ts.map +1 -1
- package/dist/index.js +68 -11
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +36 -5
- package/dist/models/CampaignMetricSnapshot.d.ts +14 -0
- package/dist/models/CampaignMetricSnapshot.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +8 -6
- package/package.json +7 -7
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
|
|
34
|
-
`dedupe_key` natural key
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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.46","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.48","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
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
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
|