@happyvertical/smrt-marketing 0.42.7 → 0.43.1
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 +14 -1
- package/README.md +45 -0
- package/dist/collections/CampaignCollection.d.ts +29 -2
- package/dist/collections/CampaignCollection.d.ts.map +1 -1
- package/dist/customer-scope.d.ts +14 -0
- package/dist/customer-scope.d.ts.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +362 -11
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +144 -8
- package/dist/models/Campaign.d.ts +5 -0
- package/dist/models/Campaign.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +67 -9
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -5
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,8 @@ publish-surface changes.
|
|
|
20
20
|
|
|
21
21
|
- **Campaign**: optional-tenant umbrella with stable natural key
|
|
22
22
|
`(tenant_id, campaign_key)`, open objective, integer-cent budget, currency,
|
|
23
|
-
schedule,
|
|
23
|
+
schedule, optional native UUID reference to the canonical commerce Customer,
|
|
24
|
+
guarded metadata helpers, and lifecycle
|
|
24
25
|
`draft → scheduled → active ↔ paused → completed → archived`. Raw saves are
|
|
25
26
|
protected by an authoritative prior-status re-read; use
|
|
26
27
|
`CampaignLifecycleService` for lifecycle writes.
|
|
@@ -52,6 +53,11 @@ All generated surfaces are explicit; never omit `api`, `mcp`, or `cli` config.
|
|
|
52
53
|
pacing prefers campaign rollups and otherwise sums channel snapshots,
|
|
53
54
|
preventing double counting without dropping channel-only periods. Status
|
|
54
55
|
uses a five-percent budget tolerance around schedule-derived expected spend.
|
|
56
|
+
- **CampaignCollection** exposes bounded, tenant-and-Customer-scoped cursor
|
|
57
|
+
pages ordered by `start_at DESC, id DESC` and bounded batch summaries for
|
|
58
|
+
total count, active count, and latest start time. Customer scope is validated
|
|
59
|
+
through runtime relationship metadata; there is no static commerce import or
|
|
60
|
+
tenant-wide campaign materialization.
|
|
55
61
|
|
|
56
62
|
## Svelte
|
|
57
63
|
|
|
@@ -66,6 +72,13 @@ render time.
|
|
|
66
72
|
|
|
67
73
|
- Runtime dependencies stay limited to `smrt-core`, `smrt-tenancy`, and
|
|
68
74
|
`smrt-ui`. Never statically import sibling domain packages.
|
|
75
|
+
- `Campaign.customerId` targets `@happyvertical/smrt-commerce:Customer` through
|
|
76
|
+
`@crossPackageRef`. Saves and customer-scoped reads require exact tenant
|
|
77
|
+
agreement (including global-to-global only) and fail without revealing
|
|
78
|
+
whether a Customer is absent or belongs elsewhere. Associated saves validate
|
|
79
|
+
and persist through one transaction-bound Campaign instance; scoped reads
|
|
80
|
+
validate and query through one fresh transaction-bound collection. PostgreSQL
|
|
81
|
+
locks the validated Customer rows for the duration of each operation.
|
|
69
82
|
- Lead loop-closing is conventional: CRM stores `sourceKind: 'campaign'` and a
|
|
70
83
|
campaign key in `sourceId`; marketing does not import or mutate Lead.
|
|
71
84
|
- Attribution math remains in sales/referrals. Marketing stores performance
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ const campaigns = await CampaignCollection.create({ db });
|
|
|
19
19
|
const channels = await CampaignChannelCollection.create({ db });
|
|
20
20
|
const campaign = await campaigns.create({
|
|
21
21
|
tenantId,
|
|
22
|
+
customerId,
|
|
22
23
|
campaignKey: 'summer-demand-2026',
|
|
23
24
|
name: 'Summer demand 2026',
|
|
24
25
|
objective: 'demand_generation',
|
|
@@ -59,6 +60,50 @@ const pacing = await BudgetPacingService.create({ db });
|
|
|
59
60
|
console.log(await pacing.getCampaignPacing(campaign.id));
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
## Customer-scoped campaign reads
|
|
64
|
+
|
|
65
|
+
`Campaign.customerId` is the native UUID relationship to the canonical
|
|
66
|
+
`@happyvertical/smrt-commerce:Customer`. A campaign and its Customer must have
|
|
67
|
+
exactly the same tenant, and customer-scoped reads require that tenant
|
|
68
|
+
explicitly (`null` selects the global/global scope). Associated Campaign saves
|
|
69
|
+
validate and persist in one transaction; customer-scoped reads validate and
|
|
70
|
+
query in one transaction. Missing and cross-tenant Customers fail with
|
|
71
|
+
`CampaignCustomerScopeError` without disclosing which condition occurred.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const firstPage = await campaigns.listByCustomer(tenantId, customerId, {
|
|
75
|
+
limit: 50,
|
|
76
|
+
});
|
|
77
|
+
const secondPage = firstPage.nextCursor
|
|
78
|
+
? await campaigns.listByCustomer(tenantId, customerId, {
|
|
79
|
+
limit: 50,
|
|
80
|
+
after: firstPage.nextCursor,
|
|
81
|
+
})
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
const summaries = await campaigns.summarizeByCustomers(tenantId, customerIds);
|
|
85
|
+
// [{ customerId, totalCount, activeCount, latestStartAt }]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Pages and summary batches are capped at 100 items and reject larger inputs.
|
|
89
|
+
Pagination is newest-first by `startAt`, then UUID; campaigns without a start
|
|
90
|
+
time follow scheduled campaigns. Summary resolution uses a bounded grouped
|
|
91
|
+
query rather than loading tenant campaigns or issuing one query per Customer.
|
|
92
|
+
|
|
93
|
+
### Migrating metadata-backed associations
|
|
94
|
+
|
|
95
|
+
1. Apply the generated schema migration that adds nullable native-UUID
|
|
96
|
+
`campaigns.customer_id` and the
|
|
97
|
+
`(tenant_id, customer_id, start_at, id)` index.
|
|
98
|
+
2. In an operator-owned data migration, extract the old metadata Customer id,
|
|
99
|
+
validate that it exists in commerce and has the exact same `tenant_id`, then
|
|
100
|
+
write `customer_id`. Stop on missing, malformed, or mismatched values.
|
|
101
|
+
3. Verify every expected association through `listByCustomer()` or
|
|
102
|
+
`summarizeByCustomers()`, then update consumers to use these APIs.
|
|
103
|
+
4. Remove the old metadata key after verification. Marketing never reads it as
|
|
104
|
+
a compatibility fallback, so there is no tenant-wide JSON or raw-SQL path to
|
|
105
|
+
keep in sync.
|
|
106
|
+
|
|
62
107
|
Svelte components are exported from `@happyvertical/smrt-marketing/svelte`.
|
|
63
108
|
They are presentational and accept plain view models; consumers remain in
|
|
64
109
|
control of fetching and mutations.
|
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
import { SmrtCollection } from '@happyvertical/smrt-core';
|
|
1
|
+
import { CollectionCacheConfig, SmrtCollection, SmrtListOptions, SmrtSelectedRow, SmrtSelectField, SmrtWhereClause } from '@happyvertical/smrt-core';
|
|
2
2
|
import { Campaign } from '../models/Campaign.js';
|
|
3
|
-
import { CampaignStatus } from '../types.js';
|
|
3
|
+
import { CampaignCustomerPage, CampaignCustomerSummary, CampaignStatus, ListCampaignsByCustomerOptions } from '../types.js';
|
|
4
|
+
export declare const MAX_CAMPAIGN_CUSTOMER_PAGE_SIZE = 100;
|
|
5
|
+
export declare const MAX_CAMPAIGN_CUSTOMER_BATCH_SIZE = 100;
|
|
4
6
|
export declare class CampaignCollection extends SmrtCollection<Campaign> {
|
|
5
7
|
static readonly _itemClass: typeof Campaign;
|
|
8
|
+
get(filter: string | SmrtWhereClause<Campaign>, options?: {
|
|
9
|
+
cache?: CollectionCacheConfig | false;
|
|
10
|
+
}): Promise<Campaign | null>;
|
|
11
|
+
list<const Select extends readonly SmrtSelectField<Campaign>[]>(options: SmrtListOptions<Campaign> & {
|
|
12
|
+
select: Select;
|
|
13
|
+
include?: never;
|
|
14
|
+
}): Promise<SmrtSelectedRow<Campaign, Select>[]>;
|
|
15
|
+
list(options?: Omit<SmrtListOptions<Campaign>, 'select'> & {
|
|
16
|
+
select?: undefined;
|
|
17
|
+
}): Promise<Campaign[]>;
|
|
6
18
|
findByCampaignKey(campaignKey: string, tenantId?: string | null): Promise<Campaign | null>;
|
|
7
19
|
findByStatus(status: CampaignStatus): Promise<Campaign[]>;
|
|
20
|
+
/**
|
|
21
|
+
* List one tenant/customer lane newest-first using a stable UUID tiebreaker.
|
|
22
|
+
* Null start times follow all scheduled rows on every supported database.
|
|
23
|
+
*/
|
|
24
|
+
listByCustomer(tenantId: string | null, customerId: string, options?: ListCampaignsByCustomerOptions): Promise<CampaignCustomerPage>;
|
|
25
|
+
private listByCustomerInTransaction;
|
|
26
|
+
/**
|
|
27
|
+
* Summarize a bounded customer batch with one scope check and one grouped
|
|
28
|
+
* aggregate query. Every requested customer receives a row, including zeros.
|
|
29
|
+
*/
|
|
30
|
+
summarizeByCustomers(tenantId: string | null, customerIds: string[]): Promise<CampaignCustomerSummary[]>;
|
|
31
|
+
private summarizeByCustomersInTransaction;
|
|
32
|
+
private inCustomerReadTransaction;
|
|
33
|
+
private listScheduledLane;
|
|
34
|
+
private listNullStartLane;
|
|
8
35
|
}
|
|
9
36
|
export default CampaignCollection;
|
|
10
37
|
//# sourceMappingURL=CampaignCollection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CampaignCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignCollection.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"CampaignCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CampaignCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAE1B,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAalC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,EAGV,oBAAoB,EACpB,uBAAuB,EACvB,cAAc,EACd,8BAA8B,EAC/B,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,+BAA+B,MAAM,CAAC;AACnD,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD,qBAAa,kBAAmB,SAAQ,cAAc,CAAC,QAAQ,CAAC;IAC9D,MAAM,CAAC,QAAQ,CAAC,UAAU,kBAAY;IAEvB,GAAG,CAChB,MAAM,EAAE,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,EAC1C,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAA;KAAO,GACtD,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAMZ,IAAI,CACjB,KAAK,CAAC,MAAM,SAAS,SAAS,eAAe,CAAC,QAAQ,CAAC,EAAE,EAEzD,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAE,GACvE,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IAChC,IAAI,CACjB,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG;QACpD,MAAM,CAAC,EAAE,SAAS,CAAC;KACpB,GACA,OAAO,CAAC,QAAQ,EAAE,CAAC;IAYhB,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;IAO/D;;;OAGG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,oBAAoB,CAAC;YAgClB,2BAA2B;IA2BzC;;;OAGG;IACG,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,uBAAuB,EAAE,CAAC;YAgCvB,iCAAiC;YAkDjC,yBAAyB;YAoBzB,iBAAiB;YA4BjB,iBAAiB;CAiBhC;AA4JD,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
interface DatabaseOptions {
|
|
3
|
+
db?: SmrtObjectOptions['db'];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Verify a bounded set of canonical commerce Customers in one database read.
|
|
7
|
+
* The generic failure deliberately does not reveal whether an id is missing or
|
|
8
|
+
* belongs to another tenant.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertCustomersBelongToTenant(options: DatabaseOptions, tenantId: string | null, customerIds: readonly string[], label: string, lock?: 'none' | 'share' | 'update'): Promise<void>;
|
|
11
|
+
/** Parse and canonicalize one UUID without echoing the rejected value. */
|
|
12
|
+
export declare function normalizeUuid(value: string, label: string): string;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=customer-scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-scope.d.ts","sourceRoot":"","sources":["../src/customer-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAWlC,UAAU,eAAe;IACvB,EAAE,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC;CAC9B;AA+BD;;;;GAIG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,MAAM,GAAG,OAAO,GAAG,QAAiB,GACzC,OAAO,CAAC,IAAI,CAAC,CAgCf;AAUD,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKlE"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,6BAA6B;gBAE9B,KAAK,EAAE,MAAM;CAI1B"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC"}
|