@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21
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 +440 -0
- package/.agent/account_management.md +455 -0
- package/.agent/action_status_updaters.md +262 -0
- package/.agent/ai_agents.md +423 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +111 -0
- package/.agent/connected_apps.md +407 -0
- package/.agent/cookbook/_fixtures/README.md +99 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
- package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
- package/.agent/cookbook/_setup/foundation.md +277 -0
- package/.agent/cookbook/_setup/healthcare.md +279 -0
- package/.agent/cookbook/_setup/payment_risk.md +283 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +557 -0
- package/.agent/data_sources.md +234 -0
- package/.agent/datalakes.md +712 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +196 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +351 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +152 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +546 -0
- package/.agent/type_naming.md +131 -0
- package/.agent/workflows.md +601 -0
- package/README.md +46 -0
- package/dist/bin/platform-sdk.d.mts +1 -0
- package/dist/bin/platform-sdk.mjs +106 -0
- package/dist/bin/platform-sdk.mjs.map +1 -0
- package/dist/index.d.mts +1200 -43201
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1859 -7319
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -10
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Healthcare industry bootstrap setup
|
|
3
|
+
summary: Auth as root, sign up + confirm a fresh industry-admin user, create a healthcare tenant + datalake. Shared by every healthcare scenario cookbook.
|
|
4
|
+
industry: healthcare
|
|
5
|
+
slug: healthcare
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/healthcare/bootstrap.test.ts
|
|
8
|
+
status: green
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Problem
|
|
12
|
+
|
|
13
|
+
Every healthcare scenario cookbook needs the same starting point:
|
|
14
|
+
a fresh tenant on the platform with a healthcare-domain datalake
|
|
15
|
+
attached, accessed by an industry-admin user. Authoring this
|
|
16
|
+
prelude inline inside every scenario cookbook would duplicate the
|
|
17
|
+
auth + tenant + datalake chain across two cookbooks (and six more
|
|
18
|
+
across the other industries). The validator auto-discovers this
|
|
19
|
+
file from each cookbook's `industry: healthcare` front-matter and
|
|
20
|
+
inlines its numbered steps ahead of the scenario's own, so the
|
|
21
|
+
generated bun:test spec is hermetic without the cookbook author
|
|
22
|
+
having to copy-paste this prelude.
|
|
23
|
+
|
|
24
|
+
# Composition
|
|
25
|
+
|
|
26
|
+
| Resource provisioned | Owner |
|
|
27
|
+
|-------------------------------|-------------|
|
|
28
|
+
| Root session | setup |
|
|
29
|
+
| Industry-admin user (`sarah`) | setup |
|
|
30
|
+
| Healthcare tenant | setup |
|
|
31
|
+
| Healthcare datalake | setup |
|
|
32
|
+
|
|
33
|
+
# Walkthrough
|
|
34
|
+
|
|
35
|
+
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
36
|
+
|
|
37
|
+
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
38
|
+
`devpassword` in local dev). Then sign up a per-run industry-admin
|
|
39
|
+
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
40
|
+
confirm them so they can sign in. Both the signup and the confirm
|
|
41
|
+
endpoints require root scope.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
ctx.rootSession = await createSession({
|
|
45
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
46
|
+
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
47
|
+
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
48
|
+
})
|
|
49
|
+
ctx.rootApi = createIsolatedPlatformApi({
|
|
50
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
51
|
+
sessionToken: ctx.rootSession.sessionToken,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
55
|
+
ctx.sarahPassword = 'CookbookPass1!'
|
|
56
|
+
|
|
57
|
+
const signUpResp = await ctx.rootApi.auth.signUp({
|
|
58
|
+
email: ctx.sarahEmail,
|
|
59
|
+
password: ctx.sarahPassword,
|
|
60
|
+
first_name: 'Cookbook',
|
|
61
|
+
last_name: 'Sarah',
|
|
62
|
+
})
|
|
63
|
+
ctx.sarahUserId = signUpResp.data.id
|
|
64
|
+
await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 002 — sarah signs in (tenantless) and creates the healthcare tenant
|
|
68
|
+
|
|
69
|
+
Sarah signs in for the first time without a tenant scope (no
|
|
70
|
+
tenant exists yet for her), then immediately creates a fresh
|
|
71
|
+
healthcare tenant. The tenant's server-derived `slug` is captured
|
|
72
|
+
into the closure-scoped `tenantSlug` slot for downstream steps.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
ctx.sarahTenantlessSession = await createSession({
|
|
76
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
77
|
+
email: ctx.sarahEmail,
|
|
78
|
+
password: ctx.sarahPassword,
|
|
79
|
+
})
|
|
80
|
+
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
81
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
82
|
+
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
86
|
+
name: `Cookbook Healthcare ${runSuffix}`,
|
|
87
|
+
})
|
|
88
|
+
tenantSlug = tenantResp.data.slug!
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 003 — sarah signs in tenant-scoped — the canonical client
|
|
92
|
+
|
|
93
|
+
Re-mint sarah's session with the new tenant slug. This bearer is
|
|
94
|
+
the canonical tenant-scoped client every subsequent step uses; it
|
|
95
|
+
is assigned to the closure-scoped `api` slot so the scenario
|
|
96
|
+
cookbook's steps inherit it via inlining.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const sarahTenantSession = await createSession({
|
|
100
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
101
|
+
email: ctx.sarahEmail,
|
|
102
|
+
password: ctx.sarahPassword,
|
|
103
|
+
tenantSlug,
|
|
104
|
+
})
|
|
105
|
+
api = createIsolatedPlatformApi({
|
|
106
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
107
|
+
sessionToken: sarahTenantSession.sessionToken,
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 004 — sarah creates the healthcare datalake
|
|
112
|
+
|
|
113
|
+
Provision a healthcare-domain datalake on the new tenant. The DB
|
|
114
|
+
schemas are scoped to the cookbook run via `runSuffix` so parallel
|
|
115
|
+
cookbook runs do not collide on schema names. Local-dev defaults
|
|
116
|
+
(`postgres` on `localhost:5432`, `alvera_dev_healthcare`) match
|
|
117
|
+
the seeded `dev.exs` setup; LocalStack S3 (`localhost:4566`)
|
|
118
|
+
serves the unregulated + regulated buckets. The datalake's
|
|
119
|
+
server-derived `slug` is captured into `datalakeSlug`.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const DB_HOST = 'localhost'
|
|
123
|
+
const DB_PORT = 5432
|
|
124
|
+
const DB_USER = 'postgres'
|
|
125
|
+
const DB_PASS = 'postgres'
|
|
126
|
+
const DB_NAME = 'alvera_dev_healthcare'
|
|
127
|
+
const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
|
|
128
|
+
const REG_SCHEMA = `cookbook_${runSuffix}_reg`
|
|
129
|
+
|
|
130
|
+
const S3 = {
|
|
131
|
+
cloud_storage_type: 'aws' as const,
|
|
132
|
+
region: 'us-east-1',
|
|
133
|
+
access_key_id: 'test',
|
|
134
|
+
secret_access_key: 'test',
|
|
135
|
+
endpoint: 'http://localhost:4566',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const datalakeResp = await api.datalakes.create(tenantSlug, {
|
|
139
|
+
name: `Cookbook Healthcare Datalake ${runSuffix}`,
|
|
140
|
+
description: 'Healthcare datalake provisioned by cookbook doctest.',
|
|
141
|
+
data_domain: 'healthcare',
|
|
142
|
+
timezone: 'America/New_York',
|
|
143
|
+
pool_size: 5,
|
|
144
|
+
|
|
145
|
+
unregulated_db_writer_host: DB_HOST,
|
|
146
|
+
unregulated_db_writer_port: DB_PORT,
|
|
147
|
+
unregulated_db_writer_name: DB_NAME,
|
|
148
|
+
unregulated_db_writer_schema: UNREG_SCHEMA,
|
|
149
|
+
unregulated_db_writer_auth_method: 'password',
|
|
150
|
+
unregulated_db_writer_user: DB_USER,
|
|
151
|
+
unregulated_db_writer_pass: DB_PASS,
|
|
152
|
+
unregulated_db_writer_enable_ssl: false,
|
|
153
|
+
unregulated_db_reader_host: DB_HOST,
|
|
154
|
+
unregulated_db_reader_port: DB_PORT,
|
|
155
|
+
unregulated_db_reader_name: DB_NAME,
|
|
156
|
+
unregulated_db_reader_schema: UNREG_SCHEMA,
|
|
157
|
+
unregulated_db_reader_auth_method: 'password',
|
|
158
|
+
unregulated_db_reader_user: DB_USER,
|
|
159
|
+
unregulated_db_reader_pass: DB_PASS,
|
|
160
|
+
unregulated_db_reader_enable_ssl: false,
|
|
161
|
+
|
|
162
|
+
regulated_data_db_writer_host: DB_HOST,
|
|
163
|
+
regulated_data_db_writer_port: DB_PORT,
|
|
164
|
+
regulated_data_db_writer_name: DB_NAME,
|
|
165
|
+
regulated_data_db_writer_schema: REG_SCHEMA,
|
|
166
|
+
regulated_data_db_writer_auth_method: 'password',
|
|
167
|
+
regulated_data_db_writer_user: DB_USER,
|
|
168
|
+
regulated_data_db_writer_pass: DB_PASS,
|
|
169
|
+
regulated_data_db_writer_enable_ssl: false,
|
|
170
|
+
regulated_data_db_reader_host: DB_HOST,
|
|
171
|
+
regulated_data_db_reader_port: DB_PORT,
|
|
172
|
+
regulated_data_db_reader_name: DB_NAME,
|
|
173
|
+
regulated_data_db_reader_schema: REG_SCHEMA,
|
|
174
|
+
regulated_data_db_reader_auth_method: 'password',
|
|
175
|
+
regulated_data_db_reader_user: DB_USER,
|
|
176
|
+
regulated_data_db_reader_pass: DB_PASS,
|
|
177
|
+
regulated_data_db_reader_enable_ssl: false,
|
|
178
|
+
|
|
179
|
+
unregulated_cloud_storage: { ...S3, bucket: 'healthcare-lake-unregulated' },
|
|
180
|
+
regulated_cloud_storage: { ...S3, bucket: 'healthcare-lake-regulated' },
|
|
181
|
+
})
|
|
182
|
+
datalakeSlug = datalakeResp.data.slug!
|
|
183
|
+
// `tools.create` and a few other endpoints accept the datalake by
|
|
184
|
+
// UUID in the request body rather than only the slug in the URL,
|
|
185
|
+
// so keep it on `ctx` for downstream steps that need it.
|
|
186
|
+
ctx.datalakeId = datalakeResp.data.id!
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 005 — enqueue the datalake migrations
|
|
190
|
+
|
|
191
|
+
`datalakes.create` persists the datalake row in `:new` status but
|
|
192
|
+
does not itself run the schema migrations. Migration is a
|
|
193
|
+
separately-triggered async job so the operator controls when the
|
|
194
|
+
(potentially slow) per-industry DDL runs. `datalakes.migrate`
|
|
195
|
+
enqueues the `DatalakeMigrationWorker` Oban job and returns
|
|
196
|
+
immediately with `status: 'enqueued'` plus the Oban `job_id`. The
|
|
197
|
+
poll in §006 then waits for that worker to finish — without this
|
|
198
|
+
call the datalake would sit at `:new` forever.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
|
|
202
|
+
if (migrateResp.data.status !== 'enqueued') {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`datalake migration not enqueued (status: ${migrateResp.data.status})`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 006 — poll the datalake until status is ready
|
|
210
|
+
|
|
211
|
+
Datalake migrations are async — the §005 migrate call enqueued a
|
|
212
|
+
`DatalakeMigrationWorker` Oban job that runs the per-industry
|
|
213
|
+
schema migrations (the regulated FHIR R4 patient + appointment
|
|
214
|
+
tables and their join tables), deploys PostgREST roles, and
|
|
215
|
+
provisions the service-account bot user. Downstream resource
|
|
216
|
+
creates (tools, workflows) only need `datalakeId` to be persisted
|
|
217
|
+
(which it is the moment §004 returns), but any scenario step that
|
|
218
|
+
ingests into the regulated/unregulated DBs or runs a dataset
|
|
219
|
+
search requires `status: 'ready'`. Polling here makes every
|
|
220
|
+
healthcare scenario cookbook deterministic regardless of how long
|
|
221
|
+
the cold migration takes on a given host; the loop exits the
|
|
222
|
+
moment the datalake reaches `:ready`, so the 5-minute cap is paid
|
|
223
|
+
only in failure mode.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const READY_TIMEOUT_MS = 5 * 60_000
|
|
227
|
+
const READY_POLL_MS = 5_000
|
|
228
|
+
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
229
|
+
let datalakeStatus: string | undefined
|
|
230
|
+
while (Date.now() < deadline) {
|
|
231
|
+
const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
|
|
232
|
+
datalakeStatus = data.status
|
|
233
|
+
if (datalakeStatus === 'ready') break
|
|
234
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
235
|
+
}
|
|
236
|
+
if (datalakeStatus !== 'ready') {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
# Rollback
|
|
244
|
+
|
|
245
|
+
The cookbook doctest harness does not currently tear down the
|
|
246
|
+
created tenant / datalake / user — each run mints fresh names via
|
|
247
|
+
`runSuffix` so reruns do not collide, and the seeded local DB is
|
|
248
|
+
cheap to reset (`mix ecto.reset` on the platform). When the
|
|
249
|
+
validator graduates to continuous integration at Milestone 18
|
|
250
|
+
(running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
|
|
251
|
+
no real state is created at all.
|
|
252
|
+
|
|
253
|
+
# Outcome
|
|
254
|
+
|
|
255
|
+
After this setup runs, the closure-scoped slots are populated as:
|
|
256
|
+
|
|
257
|
+
- `api` — a tenant-scoped `PlatformApi` client authenticated as
|
|
258
|
+
the industry-admin user
|
|
259
|
+
- `tenantSlug` — server-derived slug of the fresh healthcare
|
|
260
|
+
tenant
|
|
261
|
+
- `datalakeSlug` — server-derived slug of the fresh healthcare
|
|
262
|
+
datalake
|
|
263
|
+
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
264
|
+
credentials (kept on `ctx` because no scenario cookbook needs
|
|
265
|
+
to re-authenticate by default)
|
|
266
|
+
|
|
267
|
+
Scenario cookbooks under `industry: healthcare` start at their own
|
|
268
|
+
`§001` with these slots already populated.
|
|
269
|
+
|
|
270
|
+
# See also
|
|
271
|
+
|
|
272
|
+
- `.agent/datalakes.md` — datalake create body shape (regulated +
|
|
273
|
+
unregulated tier configuration)
|
|
274
|
+
- `.agent/AGENTS.md` § SDK auth + client construction — root vs
|
|
275
|
+
tenantless vs tenant-scoped session scopes
|
|
276
|
+
- `integration-tests/tests/healthcare/bootstrap.test.ts` — the
|
|
277
|
+
green test these snippets are lifted from (the §7b sibling
|
|
278
|
+
datalake creation and the §10–§13 sanity probes are out of
|
|
279
|
+
scope for this lean cookbook setup)
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/payment_risk/bootstrap.test.ts
|
|
8
|
+
status: green
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Problem
|
|
12
|
+
|
|
13
|
+
Every payment-risk scenario cookbook needs the same starting
|
|
14
|
+
point: a fresh tenant on the platform with a payment-risk-domain
|
|
15
|
+
datalake attached, accessed by an industry-admin user. Authoring
|
|
16
|
+
this prelude inline inside every scenario cookbook would
|
|
17
|
+
duplicate the auth + tenant + datalake chain across two cookbooks
|
|
18
|
+
(and six more across the other industries). The validator
|
|
19
|
+
auto-discovers this file from each cookbook's
|
|
20
|
+
`industry: payment_risk` front-matter and inlines its numbered
|
|
21
|
+
steps ahead of the scenario's own, so the generated bun:test spec
|
|
22
|
+
is hermetic without the cookbook author having to copy-paste this
|
|
23
|
+
prelude.
|
|
24
|
+
|
|
25
|
+
# Composition
|
|
26
|
+
|
|
27
|
+
| Resource provisioned | Owner |
|
|
28
|
+
|-------------------------------|-------------|
|
|
29
|
+
| Root session | setup |
|
|
30
|
+
| Industry-admin user (`sarah`) | setup |
|
|
31
|
+
| Payment-risk tenant | setup |
|
|
32
|
+
| Payment-risk datalake | setup |
|
|
33
|
+
|
|
34
|
+
# Walkthrough
|
|
35
|
+
|
|
36
|
+
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
37
|
+
|
|
38
|
+
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
39
|
+
`devpassword` in local dev). Then sign up a per-run industry-admin
|
|
40
|
+
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
41
|
+
confirm them so they can sign in. Both the signup and the confirm
|
|
42
|
+
endpoints require root scope.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
ctx.rootSession = await createSession({
|
|
46
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
47
|
+
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
48
|
+
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
49
|
+
})
|
|
50
|
+
ctx.rootApi = createIsolatedPlatformApi({
|
|
51
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
52
|
+
sessionToken: ctx.rootSession.sessionToken,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
56
|
+
ctx.sarahPassword = 'CookbookPass1!'
|
|
57
|
+
|
|
58
|
+
const signUpResp = await ctx.rootApi.auth.signUp({
|
|
59
|
+
email: ctx.sarahEmail,
|
|
60
|
+
password: ctx.sarahPassword,
|
|
61
|
+
first_name: 'Cookbook',
|
|
62
|
+
last_name: 'Sarah',
|
|
63
|
+
})
|
|
64
|
+
ctx.sarahUserId = signUpResp.data.id
|
|
65
|
+
await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 002 — sarah signs in (tenantless) and creates the payment-risk tenant
|
|
69
|
+
|
|
70
|
+
Sarah signs in for the first time without a tenant scope (no
|
|
71
|
+
tenant exists yet for her), then immediately creates a fresh
|
|
72
|
+
payment-risk tenant. The tenant's server-derived `slug` is
|
|
73
|
+
captured into the closure-scoped `tenantSlug` slot for downstream
|
|
74
|
+
steps.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
ctx.sarahTenantlessSession = await createSession({
|
|
78
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
79
|
+
email: ctx.sarahEmail,
|
|
80
|
+
password: ctx.sarahPassword,
|
|
81
|
+
})
|
|
82
|
+
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
83
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
84
|
+
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
88
|
+
name: `Cookbook Payment Risk ${runSuffix}`,
|
|
89
|
+
})
|
|
90
|
+
tenantSlug = tenantResp.data.slug!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 003 — sarah signs in tenant-scoped — the canonical client
|
|
94
|
+
|
|
95
|
+
Re-mint sarah's session with the new tenant slug. This bearer is
|
|
96
|
+
the canonical tenant-scoped client every subsequent step uses; it
|
|
97
|
+
is assigned to the closure-scoped `api` slot so the scenario
|
|
98
|
+
cookbook's steps inherit it via inlining.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const sarahTenantSession = await createSession({
|
|
102
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
103
|
+
email: ctx.sarahEmail,
|
|
104
|
+
password: ctx.sarahPassword,
|
|
105
|
+
tenantSlug,
|
|
106
|
+
})
|
|
107
|
+
api = createIsolatedPlatformApi({
|
|
108
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
109
|
+
sessionToken: sarahTenantSession.sessionToken,
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 004 — sarah creates the payment-risk datalake
|
|
114
|
+
|
|
115
|
+
Provision a payment-risk-domain datalake on the new tenant. The
|
|
116
|
+
DB schemas are scoped to the cookbook run via `runSuffix` so
|
|
117
|
+
parallel cookbook runs do not collide on schema names. Local-dev
|
|
118
|
+
defaults (`postgres` on `localhost:5432`, `alvera_dev_payment_risk`)
|
|
119
|
+
match the seeded `dev.exs` setup; LocalStack S3
|
|
120
|
+
(`localhost:4566`) serves the unregulated + regulated buckets.
|
|
121
|
+
The datalake's server-derived `slug` is captured into
|
|
122
|
+
`datalakeSlug`.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const DB_HOST = 'localhost'
|
|
126
|
+
const DB_PORT = 5432
|
|
127
|
+
const DB_USER = 'postgres'
|
|
128
|
+
const DB_PASS = 'postgres'
|
|
129
|
+
const DB_NAME = 'alvera_dev_payment_risk'
|
|
130
|
+
const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
|
|
131
|
+
const REG_SCHEMA = `cookbook_${runSuffix}_reg`
|
|
132
|
+
|
|
133
|
+
const S3 = {
|
|
134
|
+
cloud_storage_type: 'aws' as const,
|
|
135
|
+
region: 'us-east-1',
|
|
136
|
+
access_key_id: 'test',
|
|
137
|
+
secret_access_key: 'test',
|
|
138
|
+
endpoint: 'http://localhost:4566',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
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',
|
|
145
|
+
timezone: 'America/New_York',
|
|
146
|
+
pool_size: 5,
|
|
147
|
+
|
|
148
|
+
unregulated_db_writer_host: DB_HOST,
|
|
149
|
+
unregulated_db_writer_port: DB_PORT,
|
|
150
|
+
unregulated_db_writer_name: DB_NAME,
|
|
151
|
+
unregulated_db_writer_schema: UNREG_SCHEMA,
|
|
152
|
+
unregulated_db_writer_auth_method: 'password',
|
|
153
|
+
unregulated_db_writer_user: DB_USER,
|
|
154
|
+
unregulated_db_writer_pass: DB_PASS,
|
|
155
|
+
unregulated_db_writer_enable_ssl: false,
|
|
156
|
+
unregulated_db_reader_host: DB_HOST,
|
|
157
|
+
unregulated_db_reader_port: DB_PORT,
|
|
158
|
+
unregulated_db_reader_name: DB_NAME,
|
|
159
|
+
unregulated_db_reader_schema: UNREG_SCHEMA,
|
|
160
|
+
unregulated_db_reader_auth_method: 'password',
|
|
161
|
+
unregulated_db_reader_user: DB_USER,
|
|
162
|
+
unregulated_db_reader_pass: DB_PASS,
|
|
163
|
+
unregulated_db_reader_enable_ssl: false,
|
|
164
|
+
|
|
165
|
+
regulated_data_db_writer_host: DB_HOST,
|
|
166
|
+
regulated_data_db_writer_port: DB_PORT,
|
|
167
|
+
regulated_data_db_writer_name: DB_NAME,
|
|
168
|
+
regulated_data_db_writer_schema: REG_SCHEMA,
|
|
169
|
+
regulated_data_db_writer_auth_method: 'password',
|
|
170
|
+
regulated_data_db_writer_user: DB_USER,
|
|
171
|
+
regulated_data_db_writer_pass: DB_PASS,
|
|
172
|
+
regulated_data_db_writer_enable_ssl: false,
|
|
173
|
+
regulated_data_db_reader_host: DB_HOST,
|
|
174
|
+
regulated_data_db_reader_port: DB_PORT,
|
|
175
|
+
regulated_data_db_reader_name: DB_NAME,
|
|
176
|
+
regulated_data_db_reader_schema: REG_SCHEMA,
|
|
177
|
+
regulated_data_db_reader_auth_method: 'password',
|
|
178
|
+
regulated_data_db_reader_user: DB_USER,
|
|
179
|
+
regulated_data_db_reader_pass: DB_PASS,
|
|
180
|
+
regulated_data_db_reader_enable_ssl: false,
|
|
181
|
+
|
|
182
|
+
unregulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-unregulated' },
|
|
183
|
+
regulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-regulated' },
|
|
184
|
+
})
|
|
185
|
+
datalakeSlug = datalakeResp.data.slug!
|
|
186
|
+
// `tools.create` and a few other endpoints accept the datalake by
|
|
187
|
+
// UUID in the request body rather than only the slug in the URL,
|
|
188
|
+
// so keep it on `ctx` for downstream steps that need it.
|
|
189
|
+
ctx.datalakeId = datalakeResp.data.id!
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 005 — enqueue the datalake migrations
|
|
193
|
+
|
|
194
|
+
`datalakes.create` persists the datalake row in `:new` status but
|
|
195
|
+
does not itself run the schema migrations. Migration is a
|
|
196
|
+
separately-triggered async job so the operator controls when the
|
|
197
|
+
(potentially slow) per-industry DDL runs. `datalakes.migrate`
|
|
198
|
+
enqueues the `DatalakeMigrationWorker` Oban job and returns
|
|
199
|
+
immediately with `status: 'enqueued'` plus the Oban `job_id`. The
|
|
200
|
+
poll in §006 then waits for that worker to finish — without this
|
|
201
|
+
call the datalake would sit at `:new` forever.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
|
|
205
|
+
if (migrateResp.data.status !== 'enqueued') {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`datalake migration not enqueued (status: ${migrateResp.data.status})`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 006 — poll the datalake until status is ready
|
|
213
|
+
|
|
214
|
+
Datalake migrations are async — the §005 migrate call enqueued a
|
|
215
|
+
`DatalakeMigrationWorker` Oban job that runs the per-industry
|
|
216
|
+
schema migrations (the regulated payment-account, compliance-
|
|
217
|
+
screening, and legal-entity tables and their join tables),
|
|
218
|
+
deploys PostgREST roles, and provisions the service-account bot
|
|
219
|
+
user. Downstream resource creates (tools, workflows) only need
|
|
220
|
+
`datalakeId` to be persisted (which it is the moment §004
|
|
221
|
+
returns), but any scenario step that ingests into the
|
|
222
|
+
regulated/unregulated DBs or runs a dataset search requires
|
|
223
|
+
`status: 'ready'`. Polling here makes every payment-risk scenario
|
|
224
|
+
cookbook deterministic regardless of how long the cold migration
|
|
225
|
+
takes on a given host; the loop exits the moment the datalake
|
|
226
|
+
reaches `:ready`, so the 5-minute cap is paid only in failure
|
|
227
|
+
mode.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const READY_TIMEOUT_MS = 5 * 60_000
|
|
231
|
+
const READY_POLL_MS = 5_000
|
|
232
|
+
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
233
|
+
let datalakeStatus: string | undefined
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
|
|
236
|
+
datalakeStatus = data.status
|
|
237
|
+
if (datalakeStatus === 'ready') break
|
|
238
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
239
|
+
}
|
|
240
|
+
if (datalakeStatus !== 'ready') {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
# Rollback
|
|
248
|
+
|
|
249
|
+
The cookbook doctest harness does not currently tear down the
|
|
250
|
+
created tenant / datalake / user — each run mints fresh names via
|
|
251
|
+
`runSuffix` so reruns do not collide, and the seeded local DB is
|
|
252
|
+
cheap to reset (`mix ecto.reset` on the platform). When the
|
|
253
|
+
validator graduates to continuous integration at Milestone 18
|
|
254
|
+
(running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
|
|
255
|
+
no real state is created at all.
|
|
256
|
+
|
|
257
|
+
# Outcome
|
|
258
|
+
|
|
259
|
+
After this setup runs, the closure-scoped slots are populated as:
|
|
260
|
+
|
|
261
|
+
- `api` — a tenant-scoped `PlatformApi` client authenticated as
|
|
262
|
+
the industry-admin user
|
|
263
|
+
- `tenantSlug` — server-derived slug of the fresh payment-risk
|
|
264
|
+
tenant
|
|
265
|
+
- `datalakeSlug` — server-derived slug of the fresh payment-risk
|
|
266
|
+
datalake
|
|
267
|
+
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
268
|
+
credentials (kept on `ctx` because no scenario cookbook needs
|
|
269
|
+
to re-authenticate by default)
|
|
270
|
+
|
|
271
|
+
Scenario cookbooks under `industry: payment_risk` start at their
|
|
272
|
+
own `§001` with these slots already populated.
|
|
273
|
+
|
|
274
|
+
# See also
|
|
275
|
+
|
|
276
|
+
- `.agent/datalakes.md` — datalake create body shape (regulated +
|
|
277
|
+
unregulated tier configuration)
|
|
278
|
+
- `.agent/AGENTS.md` § SDK auth + client construction — root vs
|
|
279
|
+
tenantless vs tenant-scoped session scopes
|
|
280
|
+
- `integration-tests/tests/payment_risk/bootstrap.test.ts` — the
|
|
281
|
+
green test these snippets are lifted from (the sibling datalake
|
|
282
|
+
creation and the sanity probes are out of scope for this lean
|
|
283
|
+
cookbook setup)
|