@alvera-ai/platform-sdk 0.12.1 → 0.13.0-next.gf3d2a4a

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 (55) hide show
  1. package/.agent/AGENTS.md +121 -19
  2. package/.agent/account_management.md +79 -38
  3. package/.agent/action_logs.md +144 -0
  4. package/.agent/action_status_updaters.md +237 -53
  5. package/.agent/ai_agents.md +149 -53
  6. package/.agent/ai_sandbox.md +11 -4
  7. package/.agent/async.md +16 -6
  8. package/.agent/connected_apps.md +116 -4
  9. package/.agent/cookbook/_fixtures/README.md +9 -9
  10. package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid → payments/_compliance_screenings_payments_compliance_screening.liquid} +1 -1
  11. package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_mdm.liquid → payments/_compliance_screenings_payments_mdm.liquid} +1 -1
  12. package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_mdm.liquid → payments/_payment_accounts_payments_mdm.liquid} +1 -1
  13. package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_payment_account.liquid → payments/_payment_accounts_payments_payment_account.liquid} +1 -1
  14. package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_customer.liquid → subscription/_customers_subscription_customer.liquid} +1 -1
  15. package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_mdm.liquid → subscription/_customers_subscription_mdm.liquid} +1 -1
  16. package/.agent/cookbook/_setup/foundation.md +24 -4
  17. package/.agent/cookbook/_setup/healthcare.md +24 -4
  18. package/.agent/cookbook/_setup/{payment_risk.md → payments.md} +51 -31
  19. package/.agent/cookbook/_setup/{accounts_receivable.md → subscription.md} +51 -31
  20. package/.agent/cookbook/action-status-updaters.md +25 -18
  21. package/.agent/cookbook/bulk-ingest.md +15 -15
  22. package/.agent/cookbook/dunning-sms-for-delinquent.md +21 -21
  23. package/.agent/cookbook/{custom-tables.md → generic-tables.md} +44 -41
  24. package/.agent/cookbook/invite-team.md +19 -13
  25. package/.agent/cookbook/kyc-notification-on-account-activation.md +26 -26
  26. package/.agent/cookbook/marketing-campaign-send.md +1003 -0
  27. package/.agent/cookbook/rest-fetch.md +9 -9
  28. package/.agent/cookbook/sanctions-screening-with-agent-review.md +22 -22
  29. package/.agent/cookbook/system-templates.md +15 -15
  30. package/.agent/cookbook/triage-prospects-by-priority.md +17 -17
  31. package/.agent/cookbook/welcome-sms-for-customers.md +21 -21
  32. package/.agent/data_activation_clients.md +177 -42
  33. package/.agent/data_sources.md +55 -37
  34. package/.agent/datalakes.md +30 -17
  35. package/.agent/debugging.md +48 -14
  36. package/.agent/errors.md +39 -17
  37. package/.agent/generic_tables.md +150 -51
  38. package/.agent/interoperability_contracts.md +114 -15
  39. package/.agent/mdm.md +171 -93
  40. package/.agent/messages.md +252 -0
  41. package/.agent/templates.md +21 -4
  42. package/.agent/tool-call-configs.md +1 -1
  43. package/.agent/tools.md +150 -60
  44. package/.agent/type_naming.md +79 -18
  45. package/.agent/workflows.md +408 -62
  46. package/LICENSE +21 -93
  47. package/README.md +2 -2
  48. package/dist/bin/platform-sdk.mjs +53 -1
  49. package/dist/bin/platform-sdk.mjs.map +1 -1
  50. package/dist/index.d.mts +582 -66
  51. package/dist/index.d.mts.map +1 -1
  52. package/dist/index.mjs +654 -105
  53. package/dist/index.mjs.map +1 -1
  54. package/package.json +10 -6
  55. /package/.agent/cookbook/_fixtures/{accounts_receivable → subscription}/stripe_customers_batch1.csv +0 -0
@@ -1,23 +1,23 @@
1
1
  ---
2
- title: Payment-risk industry bootstrap setup
3
- summary: Auth as root, sign up + confirm a fresh industry-admin user, create a payment-risk tenant + datalake. Shared by every payment-risk scenario cookbook.
4
- industry: payment_risk
5
- slug: payment_risk
2
+ title: Payments industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create a payments tenant + datalake. Shared by every payments scenario cookbook.
4
+ industry: payments
5
+ slug: payments
6
6
  vitest_source:
7
- - integration-tests/tests/payment_risk/bootstrap.test.ts
7
+ - integration-tests/tests/payments/bootstrap.test.ts
8
8
  status: green
9
9
  ---
10
10
 
11
11
  # Problem
12
12
 
13
- Every payment-risk scenario cookbook needs the same starting
14
- point: a fresh tenant on the platform with a payment-risk-domain
13
+ Every payments scenario cookbook needs the same starting
14
+ point: a fresh tenant on the platform with a payments-domain
15
15
  datalake attached, accessed by an industry-admin user. Authoring
16
16
  this prelude inline inside every scenario cookbook would
17
17
  duplicate the auth + tenant + datalake chain across two cookbooks
18
18
  (and six more across the other industries). The validator
19
19
  auto-discovers this file from each cookbook's
20
- `industry: payment_risk` front-matter and inlines its numbered
20
+ `industry: payments` front-matter and inlines its numbered
21
21
  steps ahead of the scenario's own, so the generated bun:test spec
22
22
  is hermetic without the cookbook author having to copy-paste this
23
23
  prelude.
@@ -28,21 +28,23 @@ prelude.
28
28
  |-------------------------------|-------------|
29
29
  | Root session | setup |
30
30
  | Industry-admin user (`sarah`) | setup |
31
- | Payment-risk tenant | setup |
32
- | Payment-risk datalake | setup |
31
+ | Payments tenant | setup |
32
+ | Payments datalake | setup |
33
33
 
34
34
  # Walkthrough
35
35
 
36
36
  ## 001 — root admin signs in and provisions a fresh industry-admin user
37
37
 
38
38
  Authenticate as the platform's root admin (`admin@dev.local` /
39
- `devpassword` in local dev). Then sign up a per-run industry-admin
39
+ `devpassword` in local dev) via the tenantless bootstrap login —
40
+ keyless by structural necessity (no tenant exists yet to scope a
41
+ key to), and a dev/test-only surface. Then sign up a per-run industry-admin
40
42
  user (`sarah`) under a unique email derived from `runSuffix`, and
41
43
  confirm them so they can sign in. Both the signup and the confirm
42
44
  endpoints require root scope.
43
45
 
44
46
  ```typescript
45
- ctx.rootSession = await createSession({
47
+ ctx.rootSession = await createBootstrapSession({
46
48
  baseUrl: process.env.ALVERA_BASE_URL!,
47
49
  email: process.env.ALVERA_ROOT_EMAIL!,
48
50
  password: process.env.ALVERA_ROOT_PASSWORD!,
@@ -50,12 +52,13 @@ ctx.rootSession = await createSession({
50
52
  ctx.rootApi = createIsolatedPlatformApi({
51
53
  baseUrl: process.env.ALVERA_BASE_URL!,
52
54
  sessionToken: ctx.rootSession.sessionToken,
55
+ apiKey: '',
53
56
  })
54
57
 
55
58
  ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
56
59
  ctx.sarahPassword = 'CookbookPass1!'
57
60
 
58
- const signUpResp = await ctx.rootApi.auth.signUp({
61
+ const signUpResp = await ctx.rootApi.admin.signUp({
59
62
  email: ctx.sarahEmail,
60
63
  password: ctx.sarahPassword,
61
64
  first_name: 'Cookbook',
@@ -65,16 +68,16 @@ ctx.sarahUserId = signUpResp.data.id
65
68
  await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
66
69
  ```
67
70
 
68
- ## 002 — sarah signs in (tenantless) and creates the payment-risk tenant
71
+ ## 002 — sarah signs in (tenantless) and creates the payments tenant
69
72
 
70
73
  Sarah signs in for the first time without a tenant scope (no
71
74
  tenant exists yet for her), then immediately creates a fresh
72
- payment-risk tenant. The tenant's server-derived `slug` is
75
+ payments tenant. The tenant's server-derived `slug` is
73
76
  captured into the closure-scoped `tenantSlug` slot for downstream
74
77
  steps.
75
78
 
76
79
  ```typescript
77
- ctx.sarahTenantlessSession = await createSession({
80
+ ctx.sarahTenantlessSession = await createBootstrapSession({
78
81
  baseUrl: process.env.ALVERA_BASE_URL!,
79
82
  email: ctx.sarahEmail,
80
83
  password: ctx.sarahPassword,
@@ -82,10 +85,11 @@ ctx.sarahTenantlessSession = await createSession({
82
85
  ctx.sarahTenantlessApi = createIsolatedPlatformApi({
83
86
  baseUrl: process.env.ALVERA_BASE_URL!,
84
87
  sessionToken: ctx.sarahTenantlessSession.sessionToken,
88
+ apiKey: '',
85
89
  })
86
90
 
87
91
  const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
88
- name: `Cookbook Payment Risk ${runSuffix}`,
92
+ name: `Cookbook Payments ${runSuffix}`,
89
93
  })
90
94
  tenantSlug = tenantResp.data.slug!
91
95
  ```
@@ -98,24 +102,37 @@ is assigned to the closure-scoped `api` slot so the scenario
98
102
  cookbook's steps inherit it via inlining.
99
103
 
100
104
  ```typescript
105
+ // A tenant-scoped login requires X-API-Key — mint a public_api key for
106
+ // the fresh tenant via the platform-admin side door (root Bearer). In the
107
+ // web console this is Settings -> API Keys; any of the tenant's keys
108
+ // satisfies the login gate, and the minted Bearer inherits this key's
109
+ // origin policy.
110
+ const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
111
+ name: `Cookbook Bootstrap Key ${runSuffix}`,
112
+ data_access_mode: 'unregulated',
113
+ })
114
+ ctx.tenantApiKey = mintedKey.api_key
115
+
101
116
  const sarahTenantSession = await createSession({
102
117
  baseUrl: process.env.ALVERA_BASE_URL!,
103
118
  email: ctx.sarahEmail,
104
119
  password: ctx.sarahPassword,
105
120
  tenantSlug,
121
+ apiKey: ctx.tenantApiKey,
106
122
  })
107
123
  api = createIsolatedPlatformApi({
108
124
  baseUrl: process.env.ALVERA_BASE_URL!,
109
125
  sessionToken: sarahTenantSession.sessionToken,
126
+ apiKey: ctx.tenantApiKey,
110
127
  })
111
128
  ```
112
129
 
113
- ## 004 — sarah creates the payment-risk datalake
130
+ ## 004 — sarah creates the payments datalake
114
131
 
115
- Provision a payment-risk-domain datalake on the new tenant. The
132
+ Provision a payments-domain datalake on the new tenant. The
116
133
  DB schemas are scoped to the cookbook run via `runSuffix` so
117
134
  parallel cookbook runs do not collide on schema names. Local-dev
118
- defaults (`postgres` on `localhost:5432`, `alvera_dev_payment_risk`)
135
+ defaults (`postgres` on `localhost:5432`, `alvera_dev_payments`)
119
136
  match the seeded `dev.exs` setup; LocalStack S3
120
137
  (`localhost:4566`) serves the unregulated + regulated buckets.
121
138
  The datalake's server-derived `slug` is captured into
@@ -126,7 +143,7 @@ const DB_HOST = 'localhost'
126
143
  const DB_PORT = 5432
127
144
  const DB_USER = 'postgres'
128
145
  const DB_PASS = 'postgres'
129
- const DB_NAME = 'alvera_dev_payment_risk'
146
+ const DB_NAME = 'alvera_dev_payments'
130
147
  const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
131
148
  const REG_SCHEMA = `cookbook_${runSuffix}_reg`
132
149
 
@@ -139,9 +156,9 @@ const S3 = {
139
156
  }
140
157
 
141
158
  const datalakeResp = await api.datalakes.create(tenantSlug, {
142
- name: `Cookbook Payment Risk Datalake ${runSuffix}`,
143
- description: 'Payment-risk datalake provisioned by cookbook doctest.',
144
- data_domain: 'payment_risk',
159
+ name: `Cookbook Payments Datalake ${runSuffix}`,
160
+ description: 'Payments datalake provisioned by cookbook doctest.',
161
+ data_domain: 'payments',
145
162
  timezone: 'America/New_York',
146
163
  pool_size: 5,
147
164
 
@@ -179,8 +196,8 @@ const datalakeResp = await api.datalakes.create(tenantSlug, {
179
196
  regulated_data_db_reader_pass: DB_PASS,
180
197
  regulated_data_db_reader_enable_ssl: false,
181
198
 
182
- unregulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-unregulated' },
183
- regulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-regulated' },
199
+ unregulated_cloud_storage: { ...S3, bucket: 'payments-lake-unregulated' },
200
+ regulated_cloud_storage: { ...S3, bucket: 'payments-lake-regulated' },
184
201
  })
185
202
  datalakeSlug = datalakeResp.data.slug!
186
203
  // `tools.create` and a few other endpoints accept the datalake by
@@ -220,7 +237,7 @@ user. Downstream resource creates (tools, workflows) only need
220
237
  `datalakeId` to be persisted (which it is the moment §004
221
238
  returns), but any scenario step that ingests into the
222
239
  regulated/unregulated DBs or runs a dataset search requires
223
- `status: 'ready'`. Polling here makes every payment-risk scenario
240
+ `status: 'ready'`. Polling here makes every payments scenario
224
241
  cookbook deterministic regardless of how long the cold migration
225
242
  takes on a given host; the loop exits the moment the datalake
226
243
  reaches `:ready`, so the 5-minute cap is paid only in failure
@@ -260,15 +277,18 @@ After this setup runs, the closure-scoped slots are populated as:
260
277
 
261
278
  - `api` — a tenant-scoped `PlatformApi` client authenticated as
262
279
  the industry-admin user
263
- - `tenantSlug` — server-derived slug of the fresh payment-risk
280
+ - `tenantSlug` — server-derived slug of the fresh payments
264
281
  tenant
265
- - `datalakeSlug` — server-derived slug of the fresh payment-risk
282
+ - `datalakeSlug` — server-derived slug of the fresh payments
266
283
  datalake
267
284
  - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
268
285
  credentials (kept on `ctx` because no scenario cookbook needs
269
286
  to re-authenticate by default)
287
+ - `ctx.tenantApiKey` — the tenant's publishable API key (minted in
288
+ setup via `admin.createTenantApiKey`); thread it into any
289
+ additional tenant-scoped `createSession` a scenario performs
270
290
 
271
- Scenario cookbooks under `industry: payment_risk` start at their
291
+ Scenario cookbooks under `industry: payments` start at their
272
292
  own `§001` with these slots already populated.
273
293
 
274
294
  # See also
@@ -277,7 +297,7 @@ own `§001` with these slots already populated.
277
297
  unregulated tier configuration)
278
298
  - `.agent/AGENTS.md` § SDK auth + client construction — root vs
279
299
  tenantless vs tenant-scoped session scopes
280
- - `integration-tests/tests/payment_risk/bootstrap.test.ts` — the
300
+ - `integration-tests/tests/payments/bootstrap.test.ts` — the
281
301
  green test these snippets are lifted from (the sibling datalake
282
302
  creation and the sanity probes are out of scope for this lean
283
303
  cookbook setup)
@@ -1,23 +1,23 @@
1
1
  ---
2
- title: Accounts-receivable industry bootstrap setup
3
- summary: Auth as root, sign up + confirm a fresh industry-admin user, create an accounts-receivable tenant + datalake. Shared by every accounts-receivable scenario cookbook.
4
- industry: accounts_receivable
5
- slug: accounts_receivable
2
+ title: Subscription industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create an subscription tenant + datalake. Shared by every subscription scenario cookbook.
4
+ industry: subscription
5
+ slug: subscription
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/bootstrap.test.ts
8
8
  status: green
9
9
  ---
10
10
 
11
11
  # Problem
12
12
 
13
- Every accounts-receivable scenario cookbook needs the same
13
+ Every subscription scenario cookbook needs the same
14
14
  starting point: a fresh tenant on the platform with an
15
- accounts-receivable-domain datalake attached, accessed by an
15
+ subscription-domain datalake attached, accessed by an
16
16
  industry-admin user. Authoring this prelude inline inside every
17
17
  scenario cookbook would duplicate the auth + tenant + datalake
18
18
  chain across two cookbooks (and six more across the other
19
19
  industries). The validator auto-discovers this file from each
20
- cookbook's `industry: accounts_receivable` front-matter and
20
+ cookbook's `industry: subscription` front-matter and
21
21
  inlines its numbered steps ahead of the scenario's own, so the
22
22
  generated bun:test spec is hermetic without the cookbook author
23
23
  having to copy-paste this prelude.
@@ -28,21 +28,23 @@ having to copy-paste this prelude.
28
28
  |-------------------------------|-------------|
29
29
  | Root session | setup |
30
30
  | Industry-admin user (`sarah`) | setup |
31
- | Accounts-receivable tenant | setup |
32
- | Accounts-receivable datalake | setup |
31
+ | Subscription tenant | setup |
32
+ | Subscription datalake | setup |
33
33
 
34
34
  # Walkthrough
35
35
 
36
36
  ## 001 — root admin signs in and provisions a fresh industry-admin user
37
37
 
38
38
  Authenticate as the platform's root admin (`admin@dev.local` /
39
- `devpassword` in local dev). Then sign up a per-run industry-admin
39
+ `devpassword` in local dev) via the tenantless bootstrap login —
40
+ keyless by structural necessity (no tenant exists yet to scope a
41
+ key to), and a dev/test-only surface. Then sign up a per-run industry-admin
40
42
  user (`sarah`) under a unique email derived from `runSuffix`, and
41
43
  confirm them so they can sign in. Both the signup and the confirm
42
44
  endpoints require root scope.
43
45
 
44
46
  ```typescript
45
- ctx.rootSession = await createSession({
47
+ ctx.rootSession = await createBootstrapSession({
46
48
  baseUrl: process.env.ALVERA_BASE_URL!,
47
49
  email: process.env.ALVERA_ROOT_EMAIL!,
48
50
  password: process.env.ALVERA_ROOT_PASSWORD!,
@@ -50,12 +52,13 @@ ctx.rootSession = await createSession({
50
52
  ctx.rootApi = createIsolatedPlatformApi({
51
53
  baseUrl: process.env.ALVERA_BASE_URL!,
52
54
  sessionToken: ctx.rootSession.sessionToken,
55
+ apiKey: '',
53
56
  })
54
57
 
55
58
  ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
56
59
  ctx.sarahPassword = 'CookbookPass1!'
57
60
 
58
- const signUpResp = await ctx.rootApi.auth.signUp({
61
+ const signUpResp = await ctx.rootApi.admin.signUp({
59
62
  email: ctx.sarahEmail,
60
63
  password: ctx.sarahPassword,
61
64
  first_name: 'Cookbook',
@@ -65,16 +68,16 @@ ctx.sarahUserId = signUpResp.data.id
65
68
  await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
66
69
  ```
67
70
 
68
- ## 002 — sarah signs in (tenantless) and creates the accounts-receivable tenant
71
+ ## 002 — sarah signs in (tenantless) and creates the subscription tenant
69
72
 
70
73
  Sarah signs in for the first time without a tenant scope (no
71
74
  tenant exists yet for her), then immediately creates a fresh
72
- accounts-receivable tenant. The tenant's server-derived `slug` is
75
+ subscription tenant. The tenant's server-derived `slug` is
73
76
  captured into the closure-scoped `tenantSlug` slot for downstream
74
77
  steps.
75
78
 
76
79
  ```typescript
77
- ctx.sarahTenantlessSession = await createSession({
80
+ ctx.sarahTenantlessSession = await createBootstrapSession({
78
81
  baseUrl: process.env.ALVERA_BASE_URL!,
79
82
  email: ctx.sarahEmail,
80
83
  password: ctx.sarahPassword,
@@ -82,10 +85,11 @@ ctx.sarahTenantlessSession = await createSession({
82
85
  ctx.sarahTenantlessApi = createIsolatedPlatformApi({
83
86
  baseUrl: process.env.ALVERA_BASE_URL!,
84
87
  sessionToken: ctx.sarahTenantlessSession.sessionToken,
88
+ apiKey: '',
85
89
  })
86
90
 
87
91
  const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
88
- name: `Cookbook Accounts Receivable ${runSuffix}`,
92
+ name: `Cookbook Subscription ${runSuffix}`,
89
93
  })
90
94
  tenantSlug = tenantResp.data.slug!
91
95
  ```
@@ -98,25 +102,38 @@ is assigned to the closure-scoped `api` slot so the scenario
98
102
  cookbook's steps inherit it via inlining.
99
103
 
100
104
  ```typescript
105
+ // A tenant-scoped login requires X-API-Key — mint a public_api key for
106
+ // the fresh tenant via the platform-admin side door (root Bearer). In the
107
+ // web console this is Settings -> API Keys; any of the tenant's keys
108
+ // satisfies the login gate, and the minted Bearer inherits this key's
109
+ // origin policy.
110
+ const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
111
+ name: `Cookbook Bootstrap Key ${runSuffix}`,
112
+ data_access_mode: 'unregulated',
113
+ })
114
+ ctx.tenantApiKey = mintedKey.api_key
115
+
101
116
  const sarahTenantSession = await createSession({
102
117
  baseUrl: process.env.ALVERA_BASE_URL!,
103
118
  email: ctx.sarahEmail,
104
119
  password: ctx.sarahPassword,
105
120
  tenantSlug,
121
+ apiKey: ctx.tenantApiKey,
106
122
  })
107
123
  api = createIsolatedPlatformApi({
108
124
  baseUrl: process.env.ALVERA_BASE_URL!,
109
125
  sessionToken: sarahTenantSession.sessionToken,
126
+ apiKey: ctx.tenantApiKey,
110
127
  })
111
128
  ```
112
129
 
113
- ## 004 — sarah creates the accounts-receivable datalake
130
+ ## 004 — sarah creates the subscription datalake
114
131
 
115
- Provision an accounts-receivable-domain datalake on the new
132
+ Provision an subscription-domain datalake on the new
116
133
  tenant. The DB schemas are scoped to the cookbook run via
117
134
  `runSuffix` so parallel cookbook runs do not collide on schema
118
135
  names. Local-dev defaults (`postgres` on `localhost:5432`,
119
- `alvera_dev_accounts_receivable`) match the seeded `dev.exs`
136
+ `alvera_dev_subscription`) match the seeded `dev.exs`
120
137
  setup; LocalStack S3 (`localhost:4566`) serves the unregulated +
121
138
  regulated buckets. The datalake's server-derived `slug` is
122
139
  captured into `datalakeSlug`.
@@ -126,7 +143,7 @@ const DB_HOST = 'localhost'
126
143
  const DB_PORT = 5432
127
144
  const DB_USER = 'postgres'
128
145
  const DB_PASS = 'postgres'
129
- const DB_NAME = 'alvera_dev_accounts_receivable'
146
+ const DB_NAME = 'alvera_dev_subscription'
130
147
  const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
131
148
  const REG_SCHEMA = `cookbook_${runSuffix}_reg`
132
149
 
@@ -139,9 +156,9 @@ const S3 = {
139
156
  }
140
157
 
141
158
  const datalakeResp = await api.datalakes.create(tenantSlug, {
142
- name: `Cookbook Accounts Receivable Datalake ${runSuffix}`,
143
- description: 'Accounts-receivable datalake provisioned by cookbook doctest.',
144
- data_domain: 'accounts_receivable',
159
+ name: `Cookbook Subscription Datalake ${runSuffix}`,
160
+ description: 'Subscription datalake provisioned by cookbook doctest.',
161
+ data_domain: 'subscription',
145
162
  timezone: 'America/New_York',
146
163
  pool_size: 5,
147
164
 
@@ -179,8 +196,8 @@ const datalakeResp = await api.datalakes.create(tenantSlug, {
179
196
  regulated_data_db_reader_pass: DB_PASS,
180
197
  regulated_data_db_reader_enable_ssl: false,
181
198
 
182
- unregulated_cloud_storage: { ...S3, bucket: 'accounts-receivable-lake-unregulated' },
183
- regulated_cloud_storage: { ...S3, bucket: 'accounts-receivable-lake-regulated' },
199
+ unregulated_cloud_storage: { ...S3, bucket: 'subscription-lake-unregulated' },
200
+ regulated_cloud_storage: { ...S3, bucket: 'subscription-lake-regulated' },
184
201
  })
185
202
  datalakeSlug = datalakeResp.data.slug!
186
203
  // `tools.create` and a few other endpoints accept the datalake by
@@ -219,7 +236,7 @@ service-account bot user. Downstream resource creates (tools,
219
236
  workflows) only need `datalakeId` to be persisted (which it is
220
237
  the moment §004 returns), but any scenario step that ingests into
221
238
  the regulated/unregulated DBs or runs a dataset search requires
222
- `status: 'ready'`. Polling here makes every accounts-receivable
239
+ `status: 'ready'`. Polling here makes every subscription
223
240
  scenario cookbook deterministic regardless of how long the cold
224
241
  migration takes on a given host; the loop exits the moment the
225
242
  datalake reaches `:ready`, so the 5-minute cap is paid only in
@@ -260,14 +277,17 @@ After this setup runs, the closure-scoped slots are populated as:
260
277
  - `api` — a tenant-scoped `PlatformApi` client authenticated as
261
278
  the industry-admin user
262
279
  - `tenantSlug` — server-derived slug of the fresh
263
- accounts-receivable tenant
280
+ subscription tenant
264
281
  - `datalakeSlug` — server-derived slug of the fresh
265
- accounts-receivable datalake
282
+ subscription datalake
266
283
  - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
267
284
  credentials (kept on `ctx` because no scenario cookbook needs
268
285
  to re-authenticate by default)
286
+ - `ctx.tenantApiKey` — the tenant's publishable API key (minted in
287
+ setup via `admin.createTenantApiKey`); thread it into any
288
+ additional tenant-scoped `createSession` a scenario performs
269
289
 
270
- Scenario cookbooks under `industry: accounts_receivable` start at
290
+ Scenario cookbooks under `industry: subscription` start at
271
291
  their own `§001` with these slots already populated.
272
292
 
273
293
  # See also
@@ -276,7 +296,7 @@ their own `§001` with these slots already populated.
276
296
  unregulated tier configuration)
277
297
  - `.agent/AGENTS.md` § SDK auth + client construction — root vs
278
298
  tenantless vs tenant-scoped session scopes
279
- - `integration-tests/tests/accounts_receivable/bootstrap.test.ts` —
299
+ - `integration-tests/tests/subscription/bootstrap.test.ts` —
280
300
  the green test these snippets are lifted from (the sibling
281
301
  datalake creation and the sanity probes are out of scope for
282
302
  this lean cookbook setup)
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: "Capability: reconcile delivery status of the messages you send"
3
3
  summary: A capability walk for action status updaters. Register a poller tool that reads a delivery-log source (here CloudWatch via LocalStack) and an action status updater that runs on a cron, maps log events back to the messages that produced them, and updates their status (`api.actionStatusUpdaters.create`). So a "sent" SMS becomes "delivered" / "failed" without you polling by hand.
4
- industry: accounts_receivable
4
+ industry: subscription
5
5
  slug: action-status-updaters
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/action-status-updaters.test.ts
8
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/action-status-updaters.test.ts
8
+ - integration-tests/tests/subscription/bootstrap.test.ts
9
9
  status: green
10
10
  ---
11
11
 
@@ -30,7 +30,7 @@ poller-tool body.
30
30
 
31
31
  # Walkthrough
32
32
 
33
- The `_setup/accounts_receivable.md` bootstrap left `api`, `tenantSlug`,
33
+ The `_setup/subscription.md` bootstrap left `api`, `tenantSlug`,
34
34
  `datalakeSlug`, and `ctx.datalakeId` populated.
35
35
 
36
36
  ## 001 — register a data source for the tools
@@ -105,9 +105,11 @@ ctx.cwToolId = cwTool.id!
105
105
 
106
106
  The ASU ties it together: a `cron_expression` schedule, the `updater_tool_id`
107
107
  (the poller), the `sender_tool_ids` it reconciles for, and an `updater_body`
108
- naming the log group + the time window (Liquid `now` filters). The
109
- `message_config` Liquid loops the fetched `events` and emits, per event, the
110
- `external_id` of the message to update and the `set_params` (the new `status`).
108
+ naming the log group + the time window. On `start_time`/`end_time`, `now` is a
109
+ variable holding unix milliseconds — pipe it through `minutes_ago`. The
110
+ `message_config` Liquid is rendered ONCE PER EVENT, with the event itself as the
111
+ assigns (there is no `events` list to loop), and emits a FLAT object: the
112
+ `external_id` of the message to update plus the fields to set at the top level.
111
113
 
112
114
  ```typescript
113
115
  const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalakeSlug, {
@@ -125,7 +127,7 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
125
127
  },
126
128
  message_config: {
127
129
  type: 'custom',
128
- body: '{% for event in events %}{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}{% endfor %}',
130
+ body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
129
131
  },
130
132
  })
131
133
  actionStatusUpdaterId = asu.id!
@@ -158,7 +160,7 @@ const { data: ck } = await api.actionStatusUpdaters.checksum(tenantSlug, datalak
158
160
  },
159
161
  message_config: {
160
162
  type: 'custom',
161
- body: '{% for event in events %}{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}{% endfor %}',
163
+ body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
162
164
  },
163
165
  })
164
166
  if (typeof ck.checksum !== 'string' || ck.checksum.length === 0) {
@@ -193,13 +195,18 @@ if (typeof detail !== 'string' || detail.length === 0) {
193
195
  - **The poller tool's intent is `status_poller`, not the sender's intent.** The
194
196
  reconciler reads logs; it doesn't send. `sender_tool_ids` is the separate list
195
197
  of sender tools whose messages it updates.
196
- - **`message_config` maps events back to messages by `external_id`.** The Liquid
197
- emits, per fetched event, the `external_id` of the message to touch and the
198
- `set_params` to apply. Get the `external_id` field name wrong and nothing
199
- updates.
200
- - **The schedule is a cron, evaluated platform-side.** Creating the ASU registers
201
- the schedule; you don't trigger it. The `updater_body` time window
202
- (`{{ now | minutes_ago: 45 }}` … `{{ now }}`) bounds each run's log query.
198
+ - **`message_config` renders once per event, and emits a FLAT object.** The event
199
+ is the assigns — there is no `events` list to loop — and every top-level key
200
+ that is not `external_id` IS the update (`status`, `delivered_at`, …). There is
201
+ no `set_params` wrapper. Both mistakes are rejected at create.
202
+ - **The schedule is a cron, evaluated platform-side, and you CANNOT trigger it.**
203
+ Creating the ASU registers the schedule. The `updater_body` window
204
+ (`{{ now | minutes_ago: 45 }}` … `{{ now }}`) bounds each run's log query, and
205
+ must render to unix milliseconds — `now` is a variable here, not the ISO-8601
206
+ `now` filter.
207
+ - **If it doesn't reconcile, read `last_run_error` and then ESCALATE.** There is
208
+ no manual trigger and no per-run log (known limitation). Don't rewrite the
209
+ config and wait for another cron tick — see `action_status_updaters.md` §7.
203
210
  - **`checksum` parity is your drift signal.** The fingerprint of the desired body
204
211
  must match the stored row's; a mismatch means the resource drifted.
205
212
 
@@ -207,6 +214,6 @@ if (typeof detail !== 'string' || detail.length === 0) {
207
214
 
208
215
  - `action_status_updaters.md` — wire shape, `updater_body` variants, `message_config`
209
216
  - `tools.md` — `sns` sender body + `cloud_watch_log_group` poller body
210
- - `_setup/accounts_receivable.md` — the bootstrap this walk starts from
211
- - `integration-tests/tests/accounts_receivable/action-status-updaters.test.ts` —
217
+ - `_setup/subscription.md` — the bootstrap this walk starts from
218
+ - `integration-tests/tests/subscription/action-status-updaters.test.ts` —
212
219
  the green test these calls are lifted from
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  title: "Capability: load a whole file of records in one upload"
3
3
  summary: A capability walk for bulk file ingestion. Stand up an ingestion chain (data source → manual-upload tool → interop contract → DAC), mint a presigned upload URL (`api.datalakes.createUploadLink`), PUT a CSV to it, enqueue the file (`api.dataActivationClients.ingestFile`), discover the batch the worker allocates by diffing the logs, and verify every row landed (`api.datasets.search`). For backfills and exports — not row-at-a-time.
4
- industry: accounts_receivable
4
+ industry: subscription
5
5
  slug: bulk-ingest
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/run-dac-bulk.test.ts
8
- - integration-tests/tests/accounts_receivable/create-dac.test.ts
9
- - integration-tests/tests/accounts_receivable/interoperability-contracts.test.ts
10
- - integration-tests/tests/accounts_receivable/tools.test.ts
11
- - integration-tests/tests/accounts_receivable/data-sources.test.ts
12
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/run-dac-bulk.test.ts
8
+ - integration-tests/tests/subscription/create-dac.test.ts
9
+ - integration-tests/tests/subscription/interoperability-contracts.test.ts
10
+ - integration-tests/tests/subscription/tools.test.ts
11
+ - integration-tests/tests/subscription/data-sources.test.ts
12
+ - integration-tests/tests/subscription/bootstrap.test.ts
13
13
  status: green
14
14
  ---
15
15
 
@@ -33,7 +33,7 @@ entry point differs. This is **global** to every datalake. See
33
33
 
34
34
  # Walkthrough
35
35
 
36
- The `_setup/accounts_receivable.md` bootstrap left `api`, `tenantSlug`,
36
+ The `_setup/subscription.md` bootstrap left `api`, `tenantSlug`,
37
37
  `datalakeSlug`, and `ctx.datalakeId` populated. Steps 001–004 build the same
38
38
  ingestion chain any AR cookbook uses; 005–008 are the bulk-specific part.
39
39
 
@@ -81,17 +81,17 @@ rather than inlining them.
81
81
  const { readFileSync } = await import('node:fs')
82
82
  const { join } = await import('node:path')
83
83
  const customerTemplate = readFileSync(
84
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'accounts_receivable/_customers_accounts_receivable_customer.liquid'),
84
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'subscription/_customers_subscription_customer.liquid'),
85
85
  'utf8',
86
86
  )
87
87
  const mdmTemplate = readFileSync(
88
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'accounts_receivable/_customers_accounts_receivable_mdm.liquid'),
88
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'subscription/_customers_subscription_mdm.liquid'),
89
89
  'utf8',
90
90
  )
91
91
 
92
92
  const { data: contract } = await api.interoperabilityContracts.create(tenantSlug, datalakeSlug, {
93
93
  name: `Bulk Stripe Customer Contract ${runSuffix}`,
94
- description: 'Stripe customers → AccountsReceivable Customer (custom Liquid + MDM input).',
94
+ description: 'Stripe customers → Subscription Customer (custom Liquid + MDM input).',
95
95
  resource_type: 'customer',
96
96
  template_config: { type: 'custom', body: customerTemplate },
97
97
  mdm_input_config: { type: 'custom', body: mdmTemplate },
@@ -130,7 +130,7 @@ vendored 4-row Stripe customer export.
130
130
  const { readFileSync } = await import('node:fs')
131
131
  const { join } = await import('node:path')
132
132
  const csvBody = readFileSync(
133
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'accounts_receivable/stripe_customers_batch1.csv'),
133
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'subscription/stripe_customers_batch1.csv'),
134
134
  'utf8',
135
135
  )
136
136
 
@@ -248,7 +248,7 @@ if (rows.length < 4) {
248
248
 
249
249
  - `data_activation_clients.md` §6.2 — `.ingestFile` reference + the bulk flow
250
250
  - `datalakes.md` — `createUploadLink` (presigned upload) reference
251
- - `_setup/accounts_receivable.md` — the bootstrap this walk starts from
252
- - `_fixtures/accounts_receivable/` — the vendored Stripe customer CSV + Liquid
253
- - `integration-tests/tests/accounts_receivable/run-dac-bulk.test.ts` — the green
251
+ - `_setup/subscription.md` — the bootstrap this walk starts from
252
+ - `_fixtures/subscription/` — the vendored Stripe customer CSV + Liquid
253
+ - `integration-tests/tests/subscription/run-dac-bulk.test.ts` — the green
254
254
  test these calls are lifted from