@happyvertical/smrt-marketing 0.57.36 → 0.57.38

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.
@@ -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.36",
6
+ "packageVersion": "0.57.38",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "a3ca60aac4c50e886d2584315fa470ea16621f2fcfee93a7a74c7ef60fc6c1b9",
11
- "packageJson": "2d43ed5adfa6cb7f14590f8710b3c301cb9a7c0085501f23c851b50cc5363642",
12
- "agents": "3b81365608ec4f1929b51db17194f9c1f74d420b49886277e0f94b42238915d8"
10
+ "manifest": "1a3d1d8e3d96703c7362bc039fba667a75f52c0817e66b3fd2c9f2844289c6d2",
11
+ "packageJson": "7b64053421c9d3705030909548f2d04868fa0672486dd0e5d388475ecec1df16",
12
+ "agents": "65754c7b32b54e664f41ff3905773caa42b6fe730027e8340d526d583eee4012"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -346,61 +346,70 @@
346
346
  "type": "crossPackageRef",
347
347
  "required": false,
348
348
  "related": "@happyvertical/smrt-commerce:Customer",
349
- "columnType": "UUID"
349
+ "columnType": "UUID",
350
+ "description": "The customer this campaign is run for, if any."
350
351
  },
351
352
  {
352
353
  "name": "campaignKey",
353
354
  "type": "text",
354
355
  "required": true,
355
356
  "columnType": "TEXT",
356
- "default": ""
357
+ "default": "",
358
+ "description": "A short reference of your own for this campaign, kept the same once it is in use."
357
359
  },
358
360
  {
359
361
  "name": "name",
360
362
  "type": "text",
361
363
  "required": true,
362
364
  "columnType": "TEXT",
363
- "default": ""
365
+ "default": "",
366
+ "description": "What the campaign is called."
364
367
  },
365
368
  {
366
369
  "name": "objective",
367
370
  "type": "text",
368
371
  "required": false,
369
372
  "columnType": "TEXT",
370
- "default": ""
373
+ "default": "",
374
+ "description": "What the campaign is meant to achieve, such as awareness or new leads."
371
375
  },
372
376
  {
373
377
  "name": "status",
374
378
  "type": "text",
375
379
  "required": false,
376
380
  "columnType": "TEXT",
377
- "default": "draft"
381
+ "default": "draft",
382
+ "description": "Where the campaign stands, from draft through active to completed and archived."
378
383
  },
379
384
  {
380
385
  "name": "startAt",
381
386
  "type": "datetime",
382
387
  "required": false,
383
- "columnType": "TIMESTAMP"
388
+ "columnType": "TIMESTAMP",
389
+ "description": "When the campaign starts."
384
390
  },
385
391
  {
386
392
  "name": "endAt",
387
393
  "type": "datetime",
388
394
  "required": false,
389
- "columnType": "TIMESTAMP"
395
+ "columnType": "TIMESTAMP",
396
+ "description": "When the campaign ends."
390
397
  },
391
398
  {
392
399
  "name": "budgetCents",
393
400
  "type": "integer",
394
401
  "required": false,
395
402
  "columnType": "INTEGER",
396
- "default": 0
403
+ "default": 0,
404
+ "description": "The total you plan to spend on the campaign, in cents."
397
405
  },
398
406
  {
399
407
  "name": "currency",
400
408
  "type": "text",
401
409
  "required": false,
402
410
  "columnType": "TEXT",
403
- "default": "USD"
411
+ "default": "USD",
412
+ "description": "The currency every amount on this campaign is in."
404
413
  },
405
414
  {
406
415
  "name": "metadata",
@@ -439,6 +448,9 @@
439
448
  {
440
449
  "name": "save",
441
450
  "async": true,
451
+ "params": [
452
+ "options?: SmrtSaveOptions"
453
+ ],
442
454
  "returns": "Promise<this>"
443
455
  },
444
456
  {
@@ -610,47 +622,54 @@
610
622
  "type": "foreignKey",
611
623
  "required": true,
612
624
  "related": "Campaign",
613
- "columnType": "UUID"
625
+ "columnType": "UUID",
626
+ "description": "The campaign this channel runs for."
614
627
  },
615
628
  {
616
629
  "name": "channelKind",
617
630
  "type": "text",
618
631
  "required": true,
619
632
  "columnType": "TEXT",
620
- "default": ""
633
+ "default": "",
634
+ "description": "What sort of channel this is, such as an ad group, a social post, a message, content or an event."
621
635
  },
622
636
  {
623
637
  "name": "channelRef",
624
638
  "type": "text",
625
639
  "required": true,
626
640
  "columnType": "TEXT",
627
- "default": ""
641
+ "default": "",
642
+ "description": "The name or reference the channel goes by, so its results can be matched to it."
628
643
  },
629
644
  {
630
645
  "name": "allocatedBudgetCents",
631
646
  "type": "integer",
632
647
  "required": false,
633
648
  "columnType": "INTEGER",
634
- "default": 0
649
+ "default": 0,
650
+ "description": "The share of the campaign budget set aside for this channel, in cents."
635
651
  },
636
652
  {
637
653
  "name": "startAt",
638
654
  "type": "datetime",
639
655
  "required": false,
640
- "columnType": "TIMESTAMP"
656
+ "columnType": "TIMESTAMP",
657
+ "description": "When this channel starts, if different from the campaign."
641
658
  },
642
659
  {
643
660
  "name": "endAt",
644
661
  "type": "datetime",
645
662
  "required": false,
646
- "columnType": "TIMESTAMP"
663
+ "columnType": "TIMESTAMP",
664
+ "description": "When this channel ends, if different from the campaign."
647
665
  },
648
666
  {
649
667
  "name": "status",
650
668
  "type": "text",
651
669
  "required": false,
652
670
  "columnType": "TEXT",
653
- "default": "planned"
671
+ "default": "planned",
672
+ "description": "Where this channel stands, from planned through active to completed or cancelled."
654
673
  }
655
674
  ],
656
675
  "relationships": [
@@ -675,6 +694,9 @@
675
694
  {
676
695
  "name": "save",
677
696
  "async": true,
697
+ "params": [
698
+ "options?: SmrtSaveOptions"
699
+ ],
678
700
  "returns": "Promise<this>"
679
701
  }
680
702
  ],
@@ -826,81 +848,93 @@
826
848
  "type": "foreignKey",
827
849
  "required": true,
828
850
  "related": "Campaign",
829
- "columnType": "UUID"
851
+ "columnType": "UUID",
852
+ "description": "The campaign these results belong to."
830
853
  },
831
854
  {
832
855
  "name": "campaignChannelId",
833
856
  "type": "foreignKey",
834
857
  "required": false,
835
858
  "related": "CampaignChannel",
836
- "columnType": "UUID"
859
+ "columnType": "UUID",
860
+ "description": "The channel these results came from. Empty when they cover the whole campaign."
837
861
  },
838
862
  {
839
863
  "name": "periodStart",
840
864
  "type": "datetime",
841
865
  "required": true,
842
- "columnType": "TIMESTAMP"
866
+ "columnType": "TIMESTAMP",
867
+ "description": "The first day of the period these results cover."
843
868
  },
844
869
  {
845
870
  "name": "periodEnd",
846
871
  "type": "datetime",
847
872
  "required": true,
848
- "columnType": "TIMESTAMP"
873
+ "columnType": "TIMESTAMP",
874
+ "description": "The last day of the period these results cover."
849
875
  },
850
876
  {
851
877
  "name": "spendCents",
852
878
  "type": "integer",
853
879
  "required": false,
854
880
  "columnType": "INTEGER",
855
- "default": 0
881
+ "default": 0,
882
+ "description": "What was spent in the period, in cents."
856
883
  },
857
884
  {
858
885
  "name": "impressions",
859
886
  "type": "integer",
860
887
  "required": false,
861
888
  "columnType": "INTEGER",
862
- "default": 0
889
+ "default": 0,
890
+ "description": "How many times the campaign was shown."
863
891
  },
864
892
  {
865
893
  "name": "clicks",
866
894
  "type": "integer",
867
895
  "required": false,
868
896
  "columnType": "INTEGER",
869
- "default": 0
897
+ "default": 0,
898
+ "description": "How many times someone clicked through."
870
899
  },
871
900
  {
872
901
  "name": "conversions",
873
902
  "type": "integer",
874
903
  "required": false,
875
904
  "columnType": "INTEGER",
876
- "default": 0
905
+ "default": 0,
906
+ "description": "How many people did what the campaign asked, such as buying."
877
907
  },
878
908
  {
879
909
  "name": "leads",
880
910
  "type": "integer",
881
911
  "required": false,
882
912
  "columnType": "INTEGER",
883
- "default": 0
913
+ "default": 0,
914
+ "description": "How many new leads came in."
884
915
  },
885
916
  {
886
917
  "name": "revenueCents",
887
918
  "type": "integer",
888
919
  "required": false,
889
- "columnType": "INTEGER"
920
+ "columnType": "INTEGER",
921
+ "description": "The sales brought in during the period, in cents, if known."
890
922
  },
891
923
  {
892
924
  "name": "source",
893
925
  "type": "text",
894
926
  "required": true,
895
927
  "columnType": "TEXT",
896
- "default": ""
928
+ "default": "",
929
+ "description": "Where these results were reported from."
897
930
  },
898
931
  {
899
932
  "name": "dedupeKey",
900
933
  "type": "text",
901
934
  "required": true,
902
935
  "columnType": "TEXT",
903
- "default": ""
936
+ "default": "",
937
+ "description": "The reference this result is filed under, so the same report is never counted twice."
904
938
  }
905
939
  ],
906
940
  "relationships": [
@@ -938,6 +972,9 @@
938
972
  {
939
973
  "name": "save",
940
974
  "async": true,
975
+ "params": [
976
+ "options?: SmrtSaveOptions"
977
+ ],
941
978
  "returns": "Promise<this>"
942
979
  }
943
980
  ],
@@ -1280,7 +1317,212 @@
1280
1317
  "polymorphicAssociations": 0,
1281
1318
  "uuidColumns": 20
1282
1319
  },
1283
- "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## 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",
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",
1321
+ "recipes": [
1322
+ {
1323
+ "id": "marketing.campaigns",
1324
+ "className": "MarketingCampaignsRecipe",
1325
+ "label": "Campaigns",
1326
+ "summary": "Plan a marketing campaign, set its budget and dates, choose where it runs and see what it brought in.",
1327
+ "synonyms": [
1328
+ "marketing",
1329
+ "marketing campaigns",
1330
+ "promotions",
1331
+ "advertising",
1332
+ "ad campaigns",
1333
+ "campaign results"
1334
+ ],
1335
+ "models": [
1336
+ "@happyvertical/smrt-marketing:Campaign",
1337
+ "@happyvertical/smrt-marketing:CampaignChannel",
1338
+ "@happyvertical/smrt-marketing:CampaignMetricSnapshot"
1339
+ ],
1340
+ "nav": [
1341
+ {
1342
+ "label": "Campaigns",
1343
+ "model": "@happyvertical/smrt-marketing:Campaign",
1344
+ "icon": "tag",
1345
+ "description": "Every campaign, with its budget, dates and where it stands.",
1346
+ "noun": "campaign"
1347
+ },
1348
+ {
1349
+ "label": "Channels",
1350
+ "model": "@happyvertical/smrt-marketing:CampaignChannel",
1351
+ "icon": "layers",
1352
+ "description": "Where each campaign runs, such as an ad group or a social post, and its share of the budget.",
1353
+ "noun": "channel"
1354
+ }
1355
+ ],
1356
+ "requires": [],
1357
+ "section": {
1358
+ "id": "grow",
1359
+ "label": "Grow",
1360
+ "icon": "tag",
1361
+ "description": "Bring in new customers and reward the people who help."
1362
+ },
1363
+ "options": {
1364
+ "@happyvertical/smrt-marketing:Campaign": {
1365
+ "fields": {
1366
+ "name": {
1367
+ "order": 1
1368
+ },
1369
+ "objective": {
1370
+ "order": 2
1371
+ },
1372
+ "status": {
1373
+ "order": 3
1374
+ },
1375
+ "startAt": {
1376
+ "label": "Starts",
1377
+ "order": 4
1378
+ },
1379
+ "endAt": {
1380
+ "label": "Ends",
1381
+ "order": 5
1382
+ },
1383
+ "budgetCents": {
1384
+ "label": "Budget (cents)",
1385
+ "order": 6
1386
+ },
1387
+ "currency": {
1388
+ "order": 7
1389
+ },
1390
+ "customerId": {
1391
+ "label": "Customer",
1392
+ "visibility": "advanced"
1393
+ },
1394
+ "campaignKey": {
1395
+ "label": "Reference",
1396
+ "visibility": "advanced"
1397
+ },
1398
+ "tenantId": {
1399
+ "visibility": "hidden"
1400
+ },
1401
+ "metadata": {
1402
+ "visibility": "hidden"
1403
+ }
1404
+ }
1405
+ },
1406
+ "@happyvertical/smrt-marketing:CampaignChannel": {
1407
+ "fields": {
1408
+ "campaignId": {
1409
+ "label": "Campaign",
1410
+ "order": 1
1411
+ },
1412
+ "channelKind": {
1413
+ "label": "Kind",
1414
+ "order": 2
1415
+ },
1416
+ "channelRef": {
1417
+ "label": "Name",
1418
+ "order": 3
1419
+ },
1420
+ "allocatedBudgetCents": {
1421
+ "label": "Budget share (cents)",
1422
+ "order": 4
1423
+ },
1424
+ "status": {
1425
+ "order": 5
1426
+ },
1427
+ "startAt": {
1428
+ "label": "Starts",
1429
+ "order": 6
1430
+ },
1431
+ "endAt": {
1432
+ "label": "Ends",
1433
+ "order": 7
1434
+ },
1435
+ "tenantId": {
1436
+ "visibility": "hidden"
1437
+ }
1438
+ }
1439
+ },
1440
+ "@happyvertical/smrt-marketing:CampaignMetricSnapshot": {
1441
+ "fields": {
1442
+ "campaignId": {
1443
+ "label": "Campaign",
1444
+ "order": 1
1445
+ },
1446
+ "campaignChannelId": {
1447
+ "label": "Channel",
1448
+ "order": 2
1449
+ },
1450
+ "periodStart": {
1451
+ "label": "From",
1452
+ "order": 3
1453
+ },
1454
+ "periodEnd": {
1455
+ "label": "To",
1456
+ "order": 4
1457
+ },
1458
+ "spendCents": {
1459
+ "label": "Spend (cents)",
1460
+ "order": 5
1461
+ },
1462
+ "impressions": {
1463
+ "order": 6
1464
+ },
1465
+ "clicks": {
1466
+ "order": 7
1467
+ },
1468
+ "conversions": {
1469
+ "order": 8
1470
+ },
1471
+ "leads": {
1472
+ "order": 9
1473
+ },
1474
+ "revenueCents": {
1475
+ "label": "Revenue (cents)",
1476
+ "order": 10
1477
+ },
1478
+ "source": {
1479
+ "order": 11
1480
+ },
1481
+ "tenantId": {
1482
+ "visibility": "hidden"
1483
+ },
1484
+ "dedupeKey": {
1485
+ "visibility": "hidden"
1486
+ }
1487
+ }
1488
+ }
1489
+ },
1490
+ "help": {
1491
+ "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",
1492
+ "fieldRefs": [
1493
+ "Campaign.budgetCents",
1494
+ "Campaign.currency",
1495
+ "Campaign.endAt",
1496
+ "Campaign.name",
1497
+ "Campaign.objective",
1498
+ "Campaign.startAt",
1499
+ "Campaign.status",
1500
+ "CampaignChannel.allocatedBudgetCents",
1501
+ "CampaignChannel.campaignId",
1502
+ "CampaignChannel.channelKind",
1503
+ "CampaignChannel.channelRef",
1504
+ "CampaignChannel.endAt",
1505
+ "CampaignChannel.startAt",
1506
+ "CampaignMetricSnapshot.campaignChannelId",
1507
+ "CampaignMetricSnapshot.campaignId",
1508
+ "CampaignMetricSnapshot.clicks",
1509
+ "CampaignMetricSnapshot.conversions",
1510
+ "CampaignMetricSnapshot.leads",
1511
+ "CampaignMetricSnapshot.periodEnd",
1512
+ "CampaignMetricSnapshot.periodStart",
1513
+ "CampaignMetricSnapshot.revenueCents",
1514
+ "CampaignMetricSnapshot.source",
1515
+ "CampaignMetricSnapshot.spendCents"
1516
+ ]
1517
+ },
1518
+ "demo": {
1519
+ "mode": "server",
1520
+ "reasons": [
1521
+ "Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."
1522
+ ]
1523
+ }
1524
+ }
1525
+ ],
1284
1526
  "browser": {
1285
1527
  "status": "server-only",
1286
1528
  "issues": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-marketing",
3
- "version": "0.57.36",
3
+ "version": "0.57.38",
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.36",
29
- "@happyvertical/smrt-tenancy": "0.57.36",
30
- "@happyvertical/smrt-ui": "0.57.36"
28
+ "@happyvertical/smrt-core": "0.57.38",
29
+ "@happyvertical/smrt-tenancy": "0.57.38",
30
+ "@happyvertical/smrt-ui": "0.57.38"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "svelte": "^5.57.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@happyvertical/smrt-commerce": "0.57.36",
37
- "@happyvertical/smrt-vitest": "0.57.36",
38
- "@happyvertical/sql": "^0.109.0",
36
+ "@happyvertical/smrt-commerce": "0.57.38",
37
+ "@happyvertical/smrt-vitest": "0.57.38",
38
+ "@happyvertical/sql": "^0.110.0",
39
39
  "@sveltejs/package": "^2.5.8",
40
40
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
41
41
  "@testing-library/svelte": "^5.4.2",