@happyvertical/smrt-marketing 0.57.46 → 0.57.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-marketing",
5
- "packageVersion": "0.57.46",
5
+ "packageVersion": "0.57.47",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-marketing:CampaignChannelCollection": {
8
8
  "name": "campaignchannelcollection",
@@ -80,6 +80,12 @@
80
80
  "extendsTypeArg": "CampaignChannel",
81
81
  "exportName": "CampaignChannelCollection",
82
82
  "collectionExportName": "CampaignChannelCollectionCollection",
83
+ "collectionOf": "@happyvertical/smrt-marketing:CampaignChannel",
84
+ "inheritedExposure": [
85
+ "api",
86
+ "mcp",
87
+ "cli"
88
+ ],
83
89
  "schema": {
84
90
  "tableName": "campaign_channels",
85
91
  "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);",
@@ -408,6 +414,12 @@
408
414
  "extendsTypeArg": "Campaign",
409
415
  "exportName": "CampaignCollection",
410
416
  "collectionExportName": "CampaignCollectionCollection",
417
+ "collectionOf": "@happyvertical/smrt-marketing:Campaign",
418
+ "inheritedExposure": [
419
+ "api",
420
+ "mcp",
421
+ "cli"
422
+ ],
411
423
  "schema": {
412
424
  "tableName": "campaigns",
413
425
  "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);",
@@ -563,6 +575,14 @@
563
575
  "name": "dedupeKey",
564
576
  "type": "string",
565
577
  "optional": false
578
+ },
579
+ {
580
+ "name": "options",
581
+ "type": "object",
582
+ "optional": true,
583
+ "memberTypes": [
584
+ "string | null"
585
+ ]
566
586
  }
567
587
  ],
568
588
  "returnType": "Promise<CampaignMetricSnapshot | null>",
@@ -640,6 +660,12 @@
640
660
  "extendsTypeArg": "CampaignMetricSnapshot",
641
661
  "exportName": "CampaignMetricSnapshotCollection",
642
662
  "collectionExportName": "CampaignMetricSnapshotCollectionCollection",
663
+ "collectionOf": "@happyvertical/smrt-marketing:CampaignMetricSnapshot",
664
+ "inheritedExposure": [
665
+ "api",
666
+ "mcp",
667
+ "cli"
668
+ ],
643
669
  "schema": {
644
670
  "tableName": "campaign_metric_snapshots",
645
671
  "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);",
@@ -759,11 +785,13 @@
759
785
  },
760
786
  "indexes": [
761
787
  {
762
- "name": "campaign_metric_snapshots_dedupe_key_idx",
788
+ "name": "campaign_metric_snapshots_tenant_id_dedupe_key_idx",
763
789
  "columns": [
790
+ "tenant_id",
764
791
  "dedupe_key"
765
792
  ],
766
- "unique": true
793
+ "unique": true,
794
+ "nullsNotDistinct": true
767
795
  },
768
796
  {
769
797
  "name": "campaign_metric_snapshots_slug_context_idx",
@@ -1627,6 +1655,7 @@
1627
1655
  "decoratorConfig": {
1628
1656
  "tableName": "campaign_metric_snapshots",
1629
1657
  "conflictColumns": [
1658
+ "tenant_id",
1630
1659
  "dedupe_key"
1631
1660
  ],
1632
1661
  "api": {
@@ -1803,11 +1832,13 @@
1803
1832
  },
1804
1833
  "indexes": [
1805
1834
  {
1806
- "name": "campaign_metric_snapshots_dedupe_key_idx",
1835
+ "name": "campaign_metric_snapshots_tenant_id_dedupe_key_idx",
1807
1836
  "columns": [
1837
+ "tenant_id",
1808
1838
  "dedupe_key"
1809
1839
  ],
1810
- "unique": true
1840
+ "unique": true,
1841
+ "nullsNotDistinct": true
1811
1842
  },
1812
1843
  {
1813
1844
  "name": "campaign_metric_snapshots_slug_context_idx",
@@ -21,6 +21,20 @@ export declare class CampaignMetricSnapshot extends SmrtObject {
21
21
  initialize(): Promise<this>;
22
22
  save(options?: SmrtSaveOptions): Promise<this>;
23
23
  delete(): Promise<void>;
24
+ /** This row's owner: its tenant, else (before auto-populate) the context's. */
25
+ private ownerTenantId;
26
+ /**
27
+ * A tenant context may only write its own evidence (#3305 M2). Checked
28
+ * before any raw lookup so the dedupe guard cannot reveal whether another
29
+ * tenant filed a key. The class does not opt into super-admin bypass.
30
+ */
31
+ private assertOwnerInContext;
32
+ /**
33
+ * New evidence belongs to its campaign's owner (#3305 M2): a snapshot is
34
+ * never filed globally, or for another tenant, against a tenant's campaign,
35
+ * so an unnamed administrative ingest cannot fork a tenant's evidence.
36
+ */
37
+ private assertCampaignOwnedByOwner;
24
38
  private assertMetrics;
25
39
  private assertChannelMatchesCampaign;
26
40
  private serializeState;
@@ -1 +1 @@
1
- {"version":3,"file":"CampaignMetricSnapshot.d.ts","sourceRoot":"","sources":["../../src/models/CampaignMetricSnapshot.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAGL,UAAU,EACV,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAMjE,qBAQa,sBAAuB,SAAQ,UAAU;IAEpD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAI/B,UAAU,EAAE,MAAM,CAAM;IAExB,kEAAkE;IAMlE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAMxC,WAAW,EAAE,IAAI,CAAc;IAM/B,SAAS,EAAE,IAAI,CAAc;IAG7B,UAAU,EAAE,MAAM,CAAK;IAGvB,WAAW,EAAE,MAAM,CAAK;IAGxB,MAAM,EAAE,MAAM,CAAK;IAKnB,WAAW,EAAE,MAAM,CAAK;IAGxB,KAAK,EAAE,MAAM,CAAK;IAOlB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC,sEAAsE;IAKtE,MAAM,EAAE,MAAM,CAAM;IAOpB,SAAS,EAAE,MAAM,CAAM;gBAEX,OAAO,GAAE,6BAAkC;IA2BxC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA4C9C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtC,OAAO,CAAC,aAAa;YAkCP,4BAA4B;IAa1C,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,MAAM,CAAC,UAAU;CAU1B;AAED,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"CampaignMetricSnapshot.d.ts","sourceRoot":"","sources":["../../src/models/CampaignMetricSnapshot.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAGL,UAAU,EACV,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAMjE,qBAYa,sBAAuB,SAAQ,UAAU;IAEpD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAI/B,UAAU,EAAE,MAAM,CAAM;IAExB,kEAAkE;IAMlE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAMxC,WAAW,EAAE,IAAI,CAAc;IAM/B,SAAS,EAAE,IAAI,CAAc;IAG7B,UAAU,EAAE,MAAM,CAAK;IAGvB,WAAW,EAAE,MAAM,CAAK;IAGxB,MAAM,EAAE,MAAM,CAAK;IAKnB,WAAW,EAAE,MAAM,CAAK;IAGxB,KAAK,EAAE,MAAM,CAAK;IAOlB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC,sEAAsE;IAKtE,MAAM,EAAE,MAAM,CAAM;IAOpB,SAAS,EAAE,MAAM,CAAM;gBAEX,OAAO,GAAE,6BAAkC;IA2BxC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD9C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtC,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAIrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;;;OAIG;YACW,0BAA0B;IAiBxC,OAAO,CAAC,aAAa;YAkCP,4BAA4B;IAa1C,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,MAAM,CAAC,UAAU;CAU1B;AAED,eAAe,sBAAsB,CAAC"}
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-marketing",
6
- "packageVersion": "0.57.46",
6
+ "packageVersion": "0.57.47",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "ad2430e33979044080f1058ade9b1b067c80715f014c0f940d2219ecd915eac9",
11
- "packageJson": "7fa07a525c5f5239fb4c174ce474329e487168a0032c3d9ee314fa8648ba4b61",
12
- "agents": "65754c7b32b54e664f41ff3905773caa42b6fe730027e8340d526d583eee4012"
10
+ "manifest": "38e58b4b8b1c581dc0b4ba2c643f0c01a886a6e578b1c7b90491a1df184b1bf5",
11
+ "packageJson": "19055c14756a2e8fedb356e1233e475727e1a46c7aa48c4d106916dbb8e19274",
12
+ "agents": "00859623c99bd976607d6854bd23bd699255a28fb4d5229bdec099996cc14d31"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -276,7 +276,8 @@
276
276
  "name": "findByDedupeKey",
277
277
  "async": true,
278
278
  "params": [
279
- "dedupeKey: string"
279
+ "dedupeKey: string",
280
+ "options?: object"
280
281
  ],
281
282
  "returns": "Promise<CampaignMetricSnapshot | null>"
282
283
  },
@@ -984,6 +985,7 @@
984
985
  "field": "tenantId"
985
986
  },
986
987
  "conflictColumns": [
988
+ "tenant_id",
987
989
  "dedupe_key"
988
990
  ],
989
991
  "surfaces": [
@@ -1317,7 +1319,7 @@
1317
1319
  "polymorphicAssociations": 0,
1318
1320
  "uuidColumns": 20
1319
1321
  },
1320
- "agentDoc": "# @happyvertical/smrt-marketing\n\nCross-channel campaign coordination for SMRT. This package owns Campaign\nidentity, budgets, lifecycle, execution links, immutable performance evidence,\ncomputed pacing, and props-driven Svelte surfaces. It does not execute ads,\npublish social posts, send messages, or own sales attribution.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-marketing test` and\n`pnpm --filter @happyvertical/smrt-marketing typecheck`; use `test:coverage`\nto exercise the model/service and component coverage configurations.\nNatural-key changes must also run\n`pnpm --filter @happyvertical/smrt-marketing test:postgres` when\nthe disposable PostgreSQL harness is available. Use\n`pnpm --filter @happyvertical/smrt-marketing build` and `verify:pack` for\npublish-surface changes.\n\n## Models\n\n- **Campaign**: optional-tenant umbrella with stable natural key\n `(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,\n schedule, optional native UUID reference to the canonical commerce Customer,\n guarded metadata helpers, and lifecycle\n `draft → scheduled → active ↔ paused → completed → archived`. Raw saves are\n protected by an authoritative prior-status re-read; use\n `CampaignLifecycleService` for lifecycle writes.\n- **CampaignChannel**: one generic execution link per\n `(campaign_id, channel_kind, channel_ref)`. `channelKind`/`channelRef` are\n intentionally plain strings so ads, social, messages, content, events, and\n referral programs remain peer packages. Allocation and schedule overrides\n use integer cents and dates.\n- **CampaignMetricSnapshot**: immutable period evidence with a global\n `dedupe_key` natural key. A snapshot always carries `campaignId`; a null\n `campaignChannelId` means campaign-level rollup evidence; when present, the\n channel must belong to that same campaign. API/MCP/CLI expose create/list/get\n only, and the save guard rejects hydrated edits, blind-id overwrites, and\n natural-key overwrites. Programmatic deletion is rejected.\n\nEvery model is `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\nAll generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.\n\n- **Recipe `marketing.campaigns`** (`src/recipes.ts`, help in\n `src/campaigns.recipe.md`): lists campaigns and channels for the planner and\n other catalog hosts; results (snapshots) are a child record of the campaign,\n not a menu entry. Results are read-only there (snapshots are\n written by `MetricIngestionService`) and the help never tells a person to set\n the lifecycle status by hand. Fields it shows carry `@field({ description })`\n in plain words; keep them when editing the models.\n\n## Services\n\n- **CampaignLifecycleService** loads the current row before applying one legal\n transition. Paused campaigns may resume; completed campaigns may only be\n archived; archived campaigns are terminal.\n- **MetricIngestionService** validates required scope/source/period fields and\n delegates to `getOrCreateByDedupeKey()`. A replay returns the original row\n unchanged even if the replay payload differs.\n- **BudgetPacingService** computes campaign/channel pacing from snapshots and\n never persists derived balances. For each exact reporting period, campaign\n pacing prefers campaign rollups and otherwise sums channel snapshots,\n preventing double counting without dropping channel-only periods. Status\n uses a five-percent budget tolerance around schedule-derived expected spend.\n- **CampaignCollection** exposes bounded, tenant-and-Customer-scoped cursor\n pages ordered by `start_at DESC, id DESC` and bounded batch summaries for\n total count, active count, and latest start time. Its reporting-page variant\n preserves that order/cursor and adds channel count/mix, immutable metric\n totals, and canonical pacing with two page-wide grouped reads. Customer scope\n is validated through runtime relationship metadata; there is no static\n commerce import, per-campaign callback, or tenant-wide materialization.\n\n## Svelte\n\n`@happyvertical/smrt-marketing/svelte` exports `MarketingDashboard`,\n`CampaignList`, `CampaignDetail`, `ChannelMix`, and `BudgetPacing`, plus view\ninterfaces and pure helpers. Components are props-only: no fetching, no model\nimports, no providers. Use Provider-free `smrt-ui` primitives, `--smrt-*`\ntokens, named `$props()` interfaces, and integer-cent inputs formatted only at\nrender time.\n\n## Boundaries and gotchas\n\n- Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and\n `smrt-ui`. Never statically import sibling domain packages.\n- `Campaign.customerId` targets `@happyvertical/smrt-commerce:Customer` through\n `@crossPackageRef`. Saves and customer-scoped reads require exact tenant\n agreement (including global-to-global only) and fail without revealing\n whether a Customer is absent or belongs elsewhere. Associated saves validate\n and persist through one transaction-bound Campaign instance; scoped reads\n validate and query through one fresh transaction-bound collection. PostgreSQL\n locks the validated Customer rows for the duration of each operation.\n- Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a\n campaign key in `sourceId`; marketing does not import or mutate Lead.\n- Attribution math remains in sales/referrals. Marketing stores performance\n evidence only.\n- Campaign rollups and channel snapshots may describe the same period. The\n pacing service deliberately chooses one evidence level instead of summing\n both.\n- Corrections append a new metric snapshot with a new dedupe key; never edit\n persisted evidence.\n- Table names are global: `campaigns`, `campaign_channels`, and\n `campaign_metric_snapshots`.\n",
1322
+ "agentDoc": "# @happyvertical/smrt-marketing\n\nCross-channel campaign coordination for SMRT. This package owns Campaign\nidentity, budgets, lifecycle, execution links, immutable performance evidence,\ncomputed pacing, and props-driven Svelte surfaces. It does not execute ads,\npublish social posts, send messages, or own sales attribution.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-marketing test` and\n`pnpm --filter @happyvertical/smrt-marketing typecheck`; use `test:coverage`\nto exercise the model/service and component coverage configurations.\nNatural-key changes must also run\n`pnpm --filter @happyvertical/smrt-marketing test:postgres` when\nthe disposable PostgreSQL harness is available. Use\n`pnpm --filter @happyvertical/smrt-marketing build` and `verify:pack` for\npublish-surface changes.\n\n## Models\n\n- **Campaign**: optional-tenant umbrella with stable natural key\n `(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,\n schedule, optional native UUID reference to the canonical commerce Customer,\n guarded metadata helpers, and lifecycle\n `draft → scheduled → active ↔ paused → completed → archived`. Raw saves are\n protected by an authoritative prior-status re-read; use\n `CampaignLifecycleService` for lifecycle writes.\n- **CampaignChannel**: one generic execution link per\n `(campaign_id, channel_kind, channel_ref)`. `channelKind`/`channelRef` are\n intentionally plain strings so ads, social, messages, content, events, and\n referral programs remain peer packages. Allocation and schedule overrides\n use integer cents and dates.\n- **CampaignMetricSnapshot**: immutable period evidence with a tenant-local\n `(tenant_id, dedupe_key)` natural key (#3305 M2): the caller key is opaque,\n so two tenants may file the same key. `findByDedupeKey()` /\n `getOrCreateByDedupeKey()` and the save guard resolve one owner (explicit\n `tenantId`, else the context; neither → global evidence only; a foreign id\n under a context fails closed), so a system-context ingest names its tenant. A snapshot always carries `campaignId`; a null\n `campaignChannelId` means campaign-level rollup evidence; when present, the\n channel must belong to that same campaign. API/MCP/CLI expose create/list/get\n only, and the save guard rejects hydrated edits, blind-id overwrites, and\n natural-key overwrites. Programmatic deletion is rejected.\n\nEvery model is `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\nAll generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.\n\n- **Recipe `marketing.campaigns`** (`src/recipes.ts`, help in\n `src/campaigns.recipe.md`): lists campaigns and channels for the planner and\n other catalog hosts; results (snapshots) are a child record of the campaign,\n not a menu entry. Results are read-only there (snapshots are\n written by `MetricIngestionService`) and the help never tells a person to set\n the lifecycle status by hand. Fields it shows carry `@field({ description })`\n in plain words; keep them when editing the models.\n\n## Services\n\n- **CampaignLifecycleService** loads the current row before applying one legal\n transition. Paused campaigns may resume; completed campaigns may only be\n archived; archived campaigns are terminal.\n- **MetricIngestionService** validates required scope/source/period fields and\n delegates to `getOrCreateByDedupeKey()`. A replay returns the original row\n unchanged even if the replay payload differs.\n- **BudgetPacingService** computes campaign/channel pacing from snapshots and\n never persists derived balances. For each exact reporting period, campaign\n pacing prefers campaign rollups and otherwise sums channel snapshots,\n preventing double counting without dropping channel-only periods. Status\n uses a five-percent budget tolerance around schedule-derived expected spend.\n- **CampaignCollection** exposes bounded, tenant-and-Customer-scoped cursor\n pages ordered by `start_at DESC, id DESC` and bounded batch summaries for\n total count, active count, and latest start time. Its reporting-page variant\n preserves that order/cursor and adds channel count/mix, immutable metric\n totals, and canonical pacing with two page-wide grouped reads. Customer scope\n is validated through runtime relationship metadata; there is no static\n commerce import, per-campaign callback, or tenant-wide materialization.\n\n## Svelte\n\n`@happyvertical/smrt-marketing/svelte` exports `MarketingDashboard`,\n`CampaignList`, `CampaignDetail`, `ChannelMix`, and `BudgetPacing`, plus view\ninterfaces and pure helpers. Components are props-only: no fetching, no model\nimports, no providers. Use Provider-free `smrt-ui` primitives, `--smrt-*`\ntokens, named `$props()` interfaces, and integer-cent inputs formatted only at\nrender time.\n\n## Boundaries and gotchas\n\n- Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and\n `smrt-ui`. Never statically import sibling domain packages.\n- `Campaign.customerId` targets `@happyvertical/smrt-commerce:Customer` through\n `@crossPackageRef`. Saves and customer-scoped reads require exact tenant\n agreement (including global-to-global only) and fail without revealing\n whether a Customer is absent or belongs elsewhere. Associated saves validate\n and persist through one transaction-bound Campaign instance; scoped reads\n validate and query through one fresh transaction-bound collection. PostgreSQL\n locks the validated Customer rows for the duration of each operation.\n- Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a\n campaign key in `sourceId`; marketing does not import or mutate Lead.\n- Attribution math remains in sales/referrals. Marketing stores performance\n evidence only.\n- Campaign rollups and channel snapshots may describe the same period. The\n pacing service deliberately chooses one evidence level instead of summing\n both.\n- Corrections append a new metric snapshot with a new dedupe key; never edit\n persisted evidence.\n- Key rollout (#3305 M2): `smrt db:migrate` adds\n `campaign_metric_snapshots_tenant_id_dedupe_key_idx` and drops the superseded\n `campaign_metric_snapshots_dedupe_key_idx`; rows are untouched. Deploy code\n and migration together with writers drained (old code upserts against the\n dropped arbiter); a revert needs collision analysis once tenants share keys.\n Global (NULL-tenant) evidence is unique in the database only on PostgreSQL\n 15+ (NULLS NOT DISTINCT); elsewhere the model's null-aware check enforces it,\n so concurrent global ingests need PostgreSQL 15+ or a single writer.\n- New snapshots must belong to their campaign's owner, and a tenant context\n cannot file evidence for another tenant (checked before any dedupe lookup).\n- Table names are global: `campaigns`, `campaign_channels`, and\n `campaign_metric_snapshots`.\n",
1321
1323
  "recipes": [
1322
1324
  {
1323
1325
  "id": "marketing.campaigns",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-marketing",
3
- "version": "0.57.46",
3
+ "version": "0.57.47",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Cross-channel campaign coordination, immutable performance evidence, budget pacing, and reusable Svelte marketing surfaces for SMRT",
6
6
  "type": "module",
@@ -25,17 +25,17 @@
25
25
  "./manifest.json": "./dist/manifest.json"
26
26
  },
27
27
  "dependencies": {
28
- "@happyvertical/smrt-core": "0.57.46",
29
- "@happyvertical/smrt-tenancy": "0.57.46",
30
- "@happyvertical/smrt-ui": "0.57.46"
28
+ "@happyvertical/smrt-core": "0.57.47",
29
+ "@happyvertical/smrt-tenancy": "0.57.47",
30
+ "@happyvertical/smrt-ui": "0.57.47"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "svelte": "^5.57.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@happyvertical/smrt-commerce": "0.57.46",
37
- "@happyvertical/smrt-vitest": "0.57.46",
38
- "@happyvertical/sql": "^0.111.0",
36
+ "@happyvertical/smrt-commerce": "0.57.47",
37
+ "@happyvertical/smrt-vitest": "0.57.47",
38
+ "@happyvertical/sql": "^0.111.3",
39
39
  "@sveltejs/package": "^2.5.8",
40
40
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
41
41
  "@testing-library/svelte": "^5.4.2",