@happyvertical/smrt-marketing 0.39.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +79 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +67 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/CampaignChannelCollection.d.ts +9 -0
  8. package/dist/collections/CampaignChannelCollection.d.ts.map +1 -0
  9. package/dist/collections/CampaignCollection.d.ts +10 -0
  10. package/dist/collections/CampaignCollection.d.ts.map +1 -0
  11. package/dist/collections/CampaignMetricSnapshotCollection.d.ts +15 -0
  12. package/dist/collections/CampaignMetricSnapshotCollection.d.ts.map +1 -0
  13. package/dist/collections/index.d.ts +4 -0
  14. package/dist/collections/index.d.ts.map +1 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +625 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/manifest.json +1173 -0
  20. package/dist/models/Campaign.d.ts +31 -0
  21. package/dist/models/Campaign.d.ts.map +1 -0
  22. package/dist/models/CampaignChannel.d.ts +22 -0
  23. package/dist/models/CampaignChannel.d.ts.map +1 -0
  24. package/dist/models/CampaignMetricSnapshot.d.ts +29 -0
  25. package/dist/models/CampaignMetricSnapshot.d.ts.map +1 -0
  26. package/dist/models/index.d.ts +4 -0
  27. package/dist/models/index.d.ts.map +1 -0
  28. package/dist/services/BudgetPacingService.d.ts +25 -0
  29. package/dist/services/BudgetPacingService.d.ts.map +1 -0
  30. package/dist/services/CampaignLifecycleService.d.ts +19 -0
  31. package/dist/services/CampaignLifecycleService.d.ts.map +1 -0
  32. package/dist/services/MetricIngestionService.d.ts +16 -0
  33. package/dist/services/MetricIngestionService.d.ts.map +1 -0
  34. package/dist/services/index.d.ts +4 -0
  35. package/dist/services/index.d.ts.map +1 -0
  36. package/dist/smrt-knowledge.json +875 -0
  37. package/dist/svelte/__tests__/components.test.js +115 -0
  38. package/dist/svelte/__tests__/helpers.test.js +94 -0
  39. package/dist/svelte/components/BudgetPacing.svelte +129 -0
  40. package/dist/svelte/components/BudgetPacing.svelte.d.ts +10 -0
  41. package/dist/svelte/components/BudgetPacing.svelte.d.ts.map +1 -0
  42. package/dist/svelte/components/CampaignDetail.svelte +208 -0
  43. package/dist/svelte/components/CampaignDetail.svelte.d.ts +14 -0
  44. package/dist/svelte/components/CampaignDetail.svelte.d.ts.map +1 -0
  45. package/dist/svelte/components/CampaignList.svelte +140 -0
  46. package/dist/svelte/components/CampaignList.svelte.d.ts +11 -0
  47. package/dist/svelte/components/CampaignList.svelte.d.ts.map +1 -0
  48. package/dist/svelte/components/ChannelMix.svelte +138 -0
  49. package/dist/svelte/components/ChannelMix.svelte.d.ts +10 -0
  50. package/dist/svelte/components/ChannelMix.svelte.d.ts.map +1 -0
  51. package/dist/svelte/components/MarketingDashboard.svelte +129 -0
  52. package/dist/svelte/components/MarketingDashboard.svelte.d.ts +13 -0
  53. package/dist/svelte/components/MarketingDashboard.svelte.d.ts.map +1 -0
  54. package/dist/svelte/format.d.ts +8 -0
  55. package/dist/svelte/format.d.ts.map +1 -0
  56. package/dist/svelte/format.js +42 -0
  57. package/dist/svelte/index.d.ts +8 -0
  58. package/dist/svelte/index.d.ts.map +1 -0
  59. package/dist/svelte/index.js +7 -0
  60. package/dist/svelte/types.d.ts +73 -0
  61. package/dist/svelte/types.d.ts.map +1 -0
  62. package/dist/svelte/types.js +80 -0
  63. package/dist/types.d.ts +61 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +29 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +79 -0
package/AGENTS.md ADDED
@@ -0,0 +1,79 @@
1
+ # @happyvertical/smrt-marketing
2
+
3
+ Cross-channel campaign coordination for SMRT. This package owns Campaign
4
+ identity, budgets, lifecycle, execution links, immutable performance evidence,
5
+ computed pacing, and props-driven Svelte surfaces. It does not execute ads,
6
+ publish social posts, send messages, or own sales attribution.
7
+
8
+ ## Validation
9
+
10
+ Run `pnpm --filter @happyvertical/smrt-marketing test` and
11
+ `pnpm --filter @happyvertical/smrt-marketing typecheck`; use `test:coverage`
12
+ to exercise the model/service and component coverage configurations.
13
+ Natural-key changes must also run
14
+ `pnpm --filter @happyvertical/smrt-marketing test:postgres` when
15
+ the disposable PostgreSQL harness is available. Use
16
+ `pnpm --filter @happyvertical/smrt-marketing build` and `verify:pack` for
17
+ publish-surface changes.
18
+
19
+ ## Models
20
+
21
+ - **Campaign**: optional-tenant umbrella with stable natural key
22
+ `(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,
23
+ schedule, guarded metadata helpers, and lifecycle
24
+ `draft → scheduled → active ↔ paused → completed → archived`. Raw saves are
25
+ protected by an authoritative prior-status re-read; use
26
+ `CampaignLifecycleService` for lifecycle writes.
27
+ - **CampaignChannel**: one generic execution link per
28
+ `(campaign_id, channel_kind, channel_ref)`. `channelKind`/`channelRef` are
29
+ intentionally plain strings so ads, social, messages, content, events, and
30
+ referral programs remain peer packages. Allocation and schedule overrides
31
+ use integer cents and dates.
32
+ - **CampaignMetricSnapshot**: immutable period evidence with a global
33
+ `dedupe_key` natural key. A snapshot always carries `campaignId`; a null
34
+ `campaignChannelId` means campaign-level rollup evidence; when present, the
35
+ channel must belong to that same campaign. API/MCP/CLI expose create/list/get
36
+ only, and the save guard rejects hydrated edits, blind-id overwrites, and
37
+ natural-key overwrites. Programmatic deletion is rejected.
38
+
39
+ Every model is `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.
40
+ All generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.
41
+
42
+ ## Services
43
+
44
+ - **CampaignLifecycleService** loads the current row before applying one legal
45
+ transition. Paused campaigns may resume; completed campaigns may only be
46
+ archived; archived campaigns are terminal.
47
+ - **MetricIngestionService** validates required scope/source/period fields and
48
+ delegates to `getOrCreateByDedupeKey()`. A replay returns the original row
49
+ unchanged even if the replay payload differs.
50
+ - **BudgetPacingService** computes campaign/channel pacing from snapshots and
51
+ never persists derived balances. For each exact reporting period, campaign
52
+ pacing prefers campaign rollups and otherwise sums channel snapshots,
53
+ preventing double counting without dropping channel-only periods. Status
54
+ uses a five-percent budget tolerance around schedule-derived expected spend.
55
+
56
+ ## Svelte
57
+
58
+ `@happyvertical/smrt-marketing/svelte` exports `MarketingDashboard`,
59
+ `CampaignList`, `CampaignDetail`, `ChannelMix`, and `BudgetPacing`, plus view
60
+ interfaces and pure helpers. Components are props-only: no fetching, no model
61
+ imports, no providers. Use Provider-free `smrt-ui` primitives, `--smrt-*`
62
+ tokens, named `$props()` interfaces, and integer-cent inputs formatted only at
63
+ render time.
64
+
65
+ ## Boundaries and gotchas
66
+
67
+ - Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and
68
+ `smrt-ui`. Never statically import sibling domain packages.
69
+ - Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a
70
+ campaign key in `sourceId`; marketing does not import or mutate Lead.
71
+ - Attribution math remains in sales/referrals. Marketing stores performance
72
+ evidence only.
73
+ - Campaign rollups and channel snapshots may describe the same period. The
74
+ pacing service deliberately chooses one evidence level instead of summing
75
+ both.
76
+ - Corrections append a new metric snapshot with a new dedupe key; never edit
77
+ persisted evidence.
78
+ - Table names are global: `campaigns`, `campaign_channels`, and
79
+ `campaign_metric_snapshots`.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @happyvertical/smrt-marketing
2
+
3
+ Cross-channel Campaign models, immutable performance snapshots, computed
4
+ budget pacing, and reusable Svelte marketing surfaces for SMRT.
5
+
6
+ ```bash
7
+ pnpm add @happyvertical/smrt-marketing
8
+ ```
9
+
10
+ ```ts
11
+ import {
12
+ BudgetPacingService,
13
+ CampaignChannelCollection,
14
+ CampaignCollection,
15
+ MetricIngestionService,
16
+ } from '@happyvertical/smrt-marketing';
17
+
18
+ const campaigns = await CampaignCollection.create({ db });
19
+ const channels = await CampaignChannelCollection.create({ db });
20
+ const campaign = await campaigns.create({
21
+ tenantId,
22
+ campaignKey: 'summer-demand-2026',
23
+ name: 'Summer demand 2026',
24
+ objective: 'demand_generation',
25
+ budgetCents: 200_000,
26
+ currency: 'CAD',
27
+ });
28
+ if (!campaign.id) throw new Error('Campaign did not persist');
29
+
30
+ const adGroup = await channels.create({
31
+ tenantId,
32
+ campaignId: campaign.id,
33
+ channelKind: 'ad_group',
34
+ channelRef: 'ad-group-42',
35
+ allocatedBudgetCents: 150_000,
36
+ });
37
+ if (!adGroup.id) throw new Error('Campaign channel did not persist');
38
+
39
+ const ingestion = await MetricIngestionService.create({ db });
40
+ await ingestion.ingest({
41
+ tenantId,
42
+ campaignId: campaign.id,
43
+ campaignChannelId: adGroup.id,
44
+ periodStart: new Date('2026-07-01T00:00:00Z'),
45
+ periodEnd: new Date('2026-07-01T23:59:59Z'),
46
+ spendCents: 12_500,
47
+ impressions: 25_000,
48
+ clicks: 800,
49
+ conversions: 35,
50
+ leads: 20,
51
+ source: 'ad-platform',
52
+ dedupeKey: `${tenantId}:summer-demand-2026:ad-group-42:2026-07-01`,
53
+ });
54
+
55
+ // Channel-scoped evidence is accepted only when the channel belongs to the
56
+ // supplied campaign. Reporting periods are required valid date-like values.
57
+
58
+ const pacing = await BudgetPacingService.create({ db });
59
+ console.log(await pacing.getCampaignPacing(campaign.id));
60
+ ```
61
+
62
+ Svelte components are exported from `@happyvertical/smrt-marketing/svelte`.
63
+ They are presentational and accept plain view models; consumers remain in
64
+ control of fetching and mutations.
65
+
66
+ See [AGENTS.md](./AGENTS.md) for lifecycle, evidence, and package-boundary
67
+ invariants.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import { SmrtCollection } from '@happyvertical/smrt-core';
2
+ import { CampaignChannel } from '../models/CampaignChannel.js';
3
+ export declare class CampaignChannelCollection extends SmrtCollection<CampaignChannel> {
4
+ static readonly _itemClass: typeof CampaignChannel;
5
+ findByCampaign(campaignId: string): Promise<CampaignChannel[]>;
6
+ findByExecutionRef(channelKind: string, channelRef: string): Promise<CampaignChannel[]>;
7
+ }
8
+ export default CampaignChannelCollection;
9
+ //# sourceMappingURL=CampaignChannelCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignChannelCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignChannelCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;IAEvC,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAO9D,kBAAkB,CACtB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,EAAE,CAAC;CAG9B;AAED,eAAe,yBAAyB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { SmrtCollection } from '@happyvertical/smrt-core';
2
+ import { Campaign } from '../models/Campaign.js';
3
+ import { CampaignStatus } from '../types.js';
4
+ export declare class CampaignCollection extends SmrtCollection<Campaign> {
5
+ static readonly _itemClass: typeof Campaign;
6
+ findByCampaignKey(campaignKey: string, tenantId?: string | null): Promise<Campaign | null>;
7
+ findByStatus(status: CampaignStatus): Promise<Campaign[]>;
8
+ }
9
+ export default CampaignCollection;
10
+ //# sourceMappingURL=CampaignCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,qBAAa,kBAAmB,SAAQ,cAAc,CAAC,QAAQ,CAAC;IAC9D,MAAM,CAAC,QAAQ,CAAC,UAAU,kBAAY;IAEhC,iBAAiB,CACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAQrB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAMhE;AAED,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { SmrtCollection } from '@happyvertical/smrt-core';
2
+ import { CampaignMetricSnapshot } from '../models/CampaignMetricSnapshot.js';
3
+ import { CampaignMetricSnapshotOptions } from '../types.js';
4
+ export declare class CampaignMetricSnapshotCollection extends SmrtCollection<CampaignMetricSnapshot> {
5
+ static readonly _itemClass: typeof CampaignMetricSnapshot;
6
+ findByDedupeKey(dedupeKey: string): Promise<CampaignMetricSnapshot | null>;
7
+ getOrCreateByDedupeKey(options: CampaignMetricSnapshotOptions): Promise<{
8
+ snapshot: CampaignMetricSnapshot;
9
+ created: boolean;
10
+ }>;
11
+ findByCampaign(campaignId: string): Promise<CampaignMetricSnapshot[]>;
12
+ findByChannel(campaignChannelId: string): Promise<CampaignMetricSnapshot[]>;
13
+ }
14
+ export default CampaignMetricSnapshotCollection;
15
+ //# sourceMappingURL=CampaignMetricSnapshotCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignMetricSnapshotCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignMetricSnapshotCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAEjE,qBAAa,gCAAiC,SAAQ,cAAc,CAAC,sBAAsB,CAAC;IAC1F,MAAM,CAAC,QAAQ,CAAC,UAAU,gCAA0B;IAE9C,eAAe,CACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAMnC,sBAAsB,CAC1B,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC;QAAE,QAAQ,EAAE,sBAAsB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IA8B5D,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAOrE,aAAa,CACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,sBAAsB,EAAE,CAAC;CAMrC;AAED,eAAe,gCAAgC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './CampaignChannelCollection.js';
2
+ export * from './CampaignCollection.js';
3
+ export * from './CampaignMetricSnapshotCollection.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collections/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,uCAAuC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './collections/index.js';
2
+ export * from './models/index.js';
3
+ export * from './services/index.js';
4
+ export * from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,wBAAwB,CAAC;AAEhC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC"}