@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.
- package/.agent/AGENTS.md +121 -19
- package/.agent/account_management.md +79 -38
- package/.agent/action_logs.md +144 -0
- package/.agent/action_status_updaters.md +237 -53
- package/.agent/ai_agents.md +149 -53
- package/.agent/ai_sandbox.md +11 -4
- package/.agent/async.md +16 -6
- package/.agent/connected_apps.md +116 -4
- package/.agent/cookbook/_fixtures/README.md +9 -9
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid → payments/_compliance_screenings_payments_compliance_screening.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_mdm.liquid → payments/_compliance_screenings_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_mdm.liquid → payments/_payment_accounts_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_payment_account.liquid → payments/_payment_accounts_payments_payment_account.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_customer.liquid → subscription/_customers_subscription_customer.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_mdm.liquid → subscription/_customers_subscription_mdm.liquid} +1 -1
- package/.agent/cookbook/_setup/foundation.md +24 -4
- package/.agent/cookbook/_setup/healthcare.md +24 -4
- package/.agent/cookbook/_setup/{payment_risk.md → payments.md} +51 -31
- package/.agent/cookbook/_setup/{accounts_receivable.md → subscription.md} +51 -31
- package/.agent/cookbook/action-status-updaters.md +25 -18
- package/.agent/cookbook/bulk-ingest.md +15 -15
- package/.agent/cookbook/dunning-sms-for-delinquent.md +21 -21
- package/.agent/cookbook/{custom-tables.md → generic-tables.md} +44 -41
- package/.agent/cookbook/invite-team.md +19 -13
- package/.agent/cookbook/kyc-notification-on-account-activation.md +26 -26
- package/.agent/cookbook/marketing-campaign-send.md +1003 -0
- package/.agent/cookbook/rest-fetch.md +9 -9
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +22 -22
- package/.agent/cookbook/system-templates.md +15 -15
- package/.agent/cookbook/triage-prospects-by-priority.md +17 -17
- package/.agent/cookbook/welcome-sms-for-customers.md +21 -21
- package/.agent/data_activation_clients.md +177 -42
- package/.agent/data_sources.md +55 -37
- package/.agent/datalakes.md +30 -17
- package/.agent/debugging.md +48 -14
- package/.agent/errors.md +39 -17
- package/.agent/generic_tables.md +150 -51
- package/.agent/interoperability_contracts.md +114 -15
- package/.agent/mdm.md +171 -93
- package/.agent/messages.md +252 -0
- package/.agent/templates.md +21 -4
- package/.agent/tool-call-configs.md +1 -1
- package/.agent/tools.md +150 -60
- package/.agent/type_naming.md +79 -18
- package/.agent/workflows.md +408 -62
- package/LICENSE +21 -93
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +53 -1
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +582 -66
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +654 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -6
- /package/.agent/cookbook/_fixtures/{accounts_receivable → subscription}/stripe_customers_batch1.csv +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
summary: Auth as root, sign up + confirm a fresh industry-admin user, create a
|
|
4
|
-
industry:
|
|
5
|
-
slug:
|
|
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/
|
|
7
|
+
- integration-tests/tests/payments/bootstrap.test.ts
|
|
8
8
|
status: green
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Problem
|
|
12
12
|
|
|
13
|
-
Every
|
|
14
|
-
point: a fresh tenant on the platform with a
|
|
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:
|
|
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
|
-
|
|
|
32
|
-
|
|
|
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)
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
130
|
+
## 004 — sarah creates the payments datalake
|
|
114
131
|
|
|
115
|
-
Provision a
|
|
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`, `
|
|
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 = '
|
|
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
|
|
143
|
-
description: '
|
|
144
|
-
data_domain: '
|
|
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: '
|
|
183
|
-
regulated_cloud_storage: { ...S3, bucket: '
|
|
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
|
|
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
|
|
280
|
+
- `tenantSlug` — server-derived slug of the fresh payments
|
|
264
281
|
tenant
|
|
265
|
-
- `datalakeSlug` — server-derived slug of the fresh
|
|
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:
|
|
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/
|
|
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:
|
|
3
|
-
summary: Auth as root, sign up + confirm a fresh industry-admin user, create an
|
|
4
|
-
industry:
|
|
5
|
-
slug:
|
|
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/
|
|
7
|
+
- integration-tests/tests/subscription/bootstrap.test.ts
|
|
8
8
|
status: green
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Problem
|
|
12
12
|
|
|
13
|
-
Every
|
|
13
|
+
Every subscription scenario cookbook needs the same
|
|
14
14
|
starting point: a fresh tenant on the platform with an
|
|
15
|
-
|
|
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:
|
|
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
|
-
|
|
|
32
|
-
|
|
|
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)
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
130
|
+
## 004 — sarah creates the subscription datalake
|
|
114
131
|
|
|
115
|
-
Provision an
|
|
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
|
-
`
|
|
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 = '
|
|
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
|
|
143
|
-
description: '
|
|
144
|
-
data_domain: '
|
|
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: '
|
|
183
|
-
regulated_cloud_storage: { ...S3, bucket: '
|
|
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
|
|
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
|
-
|
|
280
|
+
subscription tenant
|
|
264
281
|
- `datalakeSlug` — server-derived slug of the fresh
|
|
265
|
-
|
|
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:
|
|
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/
|
|
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:
|
|
4
|
+
industry: subscription
|
|
5
5
|
slug: action-status-updaters
|
|
6
6
|
vitest_source:
|
|
7
|
-
- integration-tests/tests/
|
|
8
|
-
- integration-tests/tests/
|
|
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/
|
|
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
|
|
109
|
-
|
|
110
|
-
`
|
|
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: '{
|
|
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: '{
|
|
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`
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
- **The schedule is a cron, evaluated platform-side
|
|
201
|
-
the
|
|
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/
|
|
211
|
-
- `integration-tests/tests/
|
|
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:
|
|
4
|
+
industry: subscription
|
|
5
5
|
slug: bulk-ingest
|
|
6
6
|
vitest_source:
|
|
7
|
-
- integration-tests/tests/
|
|
8
|
-
- integration-tests/tests/
|
|
9
|
-
- integration-tests/tests/
|
|
10
|
-
- integration-tests/tests/
|
|
11
|
-
- integration-tests/tests/
|
|
12
|
-
- integration-tests/tests/
|
|
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/
|
|
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!, '
|
|
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!, '
|
|
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 →
|
|
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!, '
|
|
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/
|
|
252
|
-
- `_fixtures/
|
|
253
|
-
- `integration-tests/tests/
|
|
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
|