@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,16 +1,16 @@
1
1
  ---
2
2
  title: Send a payment-reminder SMS to delinquent customers with a pay-invoice link
3
3
  summary: End-to-end standard workflow — provision a Dunning SMS workflow over the AR customer dataset, ingest two Stripe customer rows (one phone + tax-id verified, one missing tax-id) through a customer interop contract, run the workflow so the SMS-reachable-and-KYC-complete filter routes each row, read the rendered SMS back from the message dataset, and close the connected-app reply loop. No agent, no LLM.
4
- industry: accounts_receivable
4
+ industry: subscription
5
5
  slug: dunning-sms-for-delinquent
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/dunning-sms-workflow.test.ts
8
- - integration-tests/tests/accounts_receivable/interoperability-contracts.test.ts
9
- - integration-tests/tests/accounts_receivable/run-dac-single.test.ts
10
- - integration-tests/tests/accounts_receivable/create-dac.test.ts
11
- - integration-tests/tests/accounts_receivable/tools.test.ts
12
- - integration-tests/tests/accounts_receivable/data-sources.test.ts
13
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/dunning-sms-workflow.test.ts
8
+ - integration-tests/tests/subscription/interoperability-contracts.test.ts
9
+ - integration-tests/tests/subscription/run-dac-single.test.ts
10
+ - integration-tests/tests/subscription/create-dac.test.ts
11
+ - integration-tests/tests/subscription/tools.test.ts
12
+ - integration-tests/tests/subscription/data-sources.test.ts
13
+ - integration-tests/tests/subscription/bootstrap.test.ts
14
14
  status: green
15
15
  ---
16
16
 
@@ -45,10 +45,10 @@ connected-app deep-link the SMS carries — closing the SMS → reply
45
45
  loop.
46
46
 
47
47
  The scenario is anchored to
48
- `platform/integration-tests/tests/accounts_receivable/dunning-sms-workflow.test.ts`
48
+ `platform/integration-tests/tests/subscription/dunning-sms-workflow.test.ts`
49
49
  — a green end-to-end test (§1–§8) that exercises this exact shape.
50
50
  This cookbook is a prose re-presentation of what that test walks.
51
- The setup file `_setup/accounts_receivable.md` already provisioned
51
+ The setup file `_setup/subscription.md` already provisioned
52
52
  the tenant, datalake, and industry-admin client; this cookbook
53
53
  starts from there.
54
54
 
@@ -64,7 +64,7 @@ starts from there.
64
64
  | Stripe Customer interop contract | build |
65
65
  | Manual-upload DAC | build |
66
66
 
67
- The setup file `_setup/accounts_receivable.md` already
67
+ The setup file `_setup/subscription.md` already
68
68
  provisioned the tenant + datalake + tenant-scoped client; this
69
69
  cookbook starts from there.
70
70
 
@@ -269,17 +269,17 @@ customer or an existing one.
269
269
  const { readFileSync } = await import('node:fs')
270
270
  const { join } = await import('node:path')
271
271
  const customerTemplate = readFileSync(
272
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'accounts_receivable/_customers_accounts_receivable_customer.liquid'),
272
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'subscription/_customers_subscription_customer.liquid'),
273
273
  'utf8',
274
274
  )
275
275
  const mdmTemplate = readFileSync(
276
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'accounts_receivable/_customers_accounts_receivable_mdm.liquid'),
276
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'subscription/_customers_subscription_mdm.liquid'),
277
277
  'utf8',
278
278
  )
279
279
 
280
280
  const contractResp = await api.interoperabilityContracts.create(tenantSlug, datalakeSlug, {
281
281
  name: `Cookbook Stripe Customer Contract ${runSuffix}`,
282
- description: 'Stripe customers → AccountsReceivable Customer (custom Liquid + MDM input).',
282
+ description: 'Stripe customers → Subscription Customer (custom Liquid + MDM input).',
283
283
  resource_type: 'customer',
284
284
  template_config: { type: 'custom', body: customerTemplate },
285
285
  mdm_input_config: { type: 'custom', body: mdmTemplate },
@@ -561,7 +561,7 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
561
561
  # Rollback
562
562
 
563
563
  The cookbook doctest harness does not currently tear down created
564
- resources. The `_setup/accounts_receivable.md` setup file's
564
+ resources. The `_setup/subscription.md` setup file's
565
565
  runSuffix-scoped tenant / datalake / user names mean each run is
566
566
  naturally isolated; the seeded local DB is cheap to reset
567
567
  (`mix ecto.reset` on the platform repo).
@@ -570,8 +570,8 @@ naturally isolated; the seeded local DB is cheap to reset
570
570
 
571
571
  After this cookbook's thirteen steps run green:
572
572
 
573
- - An accounts-receivable tenant exists with an
574
- accounts-receivable-domain datalake
573
+ - An subscription tenant exists with an
574
+ subscription-domain datalake
575
575
  - An SMS tool, a Dunning payment-portal connected app, and a
576
576
  Dunning SMS standard workflow (status `live`, `customer`
577
577
  dataset) are registered
@@ -595,7 +595,7 @@ link — is demonstrated end-to-end, not merely provisioned.
595
595
 
596
596
  # See also
597
597
 
598
- - `_setup/accounts_receivable.md` — the inlined bootstrap that
598
+ - `_setup/subscription.md` — the inlined bootstrap that
599
599
  provisions the tenant + datalake this cookbook starts from
600
600
  - `welcome-sms-for-customers.md` — the sibling AR cookbook; same
601
601
  customer-dataset shape, a simpler phone-only filter
@@ -609,11 +609,11 @@ link — is demonstrated end-to-end, not merely provisioned.
609
609
  interop contract → DAC ingestion chain
610
610
  - `.agent/workflows.md` — standard workflow primitive (filter +
611
611
  decision + actions), `context_datasets`, `workflows.run`
612
- - `.agent/cookbook/_fixtures/accounts_receivable/` — the vendored
612
+ - `.agent/cookbook/_fixtures/subscription/` — the vendored
613
613
  Stripe customer / MDM Liquid templates §006 loads
614
- - `integration-tests/tests/accounts_receivable/dunning-sms-workflow.test.ts` —
614
+ - `integration-tests/tests/subscription/dunning-sms-workflow.test.ts` —
615
615
  the anchor green test (§1–§8) these snippets are lifted from
616
- - `integration-tests/tests/accounts_receivable/interoperability-contracts.test.ts`,
616
+ - `integration-tests/tests/subscription/interoperability-contracts.test.ts`,
617
617
  `run-dac-single.test.ts`, `create-dac.test.ts`, `tools.test.ts`,
618
618
  `data-sources.test.ts` — the per-resource create + ingest
619
619
  snippets §004–§009 are lifted from
@@ -1,11 +1,11 @@
1
1
  ---
2
- title: "Capability: stand up a custom table the platform doesn't model"
3
- summary: A capability walk for generic tables. Create a custom table with typed, privacy-tagged columns (`api.genericTables.create`), wait for it to deploy, use the default ingestion client the platform auto-provisions for it, ingest a row (`api.dataActivationClients.ingest`), and read it back by its unique key (`api.datasets.search`). For data the built-in datasets don't cover.
4
- industry: accounts_receivable
5
- slug: custom-tables
2
+ title: "Capability: stand up a generic table the platform doesn't model"
3
+ summary: A capability walk for generic tables. Create a generic table with typed, privacy-tagged columns (`api.genericTables.create`), wait for it to deploy, use the default ingestion client the platform auto-provisions for it, ingest a row (`api.dataActivationClients.ingest`), and read it back with read-only SQL (`api.datalakes.executeSql`). For data the built-in datasets don't cover.
4
+ industry: subscription
5
+ slug: generic-tables
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/generic-tables.test.ts
8
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/generic-tables.test.ts
8
+ - integration-tests/tests/subscription/bootstrap.test.ts
9
9
  status: green
10
10
  ---
11
11
 
@@ -23,17 +23,17 @@ The lifecycle is:
23
23
  **auto-provisions a default Data Activation Client** bound to an identity
24
24
  contract — no tool, contract, or DAC to wire by hand.
25
25
  3. `dataActivationClients.ingest(...)` a row through that default client.
26
- 4. `datasets.search(...)` it back by its unique column.
26
+ 4. `datalakes.executeSql(...)` reads it back by its unique column with read-only SQL.
27
27
 
28
28
  This is **global** to every datalake — only the table's domain meaning differs.
29
29
  See `generic_tables.md` for the column reference.
30
30
 
31
31
  # Walkthrough
32
32
 
33
- The `_setup/accounts_receivable.md` bootstrap left `api`, `tenantSlug`, and
33
+ The `_setup/subscription.md` bootstrap left `api`, `tenantSlug`, and
34
34
  `datalakeSlug` populated.
35
35
 
36
- ## 001 — list existing custom tables (sanity)
36
+ ## 001 — list existing generic tables (sanity)
37
37
 
38
38
  `genericTables.list` returns the paginated `{ data, meta }` envelope. A fresh
39
39
  datalake has none; the call just confirms the surface is reachable.
@@ -45,7 +45,7 @@ if (!Array.isArray(existing.data)) {
45
45
  }
46
46
  ```
47
47
 
48
- ## 002 — create the custom table
48
+ ## 002 — create the generic table
49
49
 
50
50
  Declare the table with a `title` and `columns`. Each column has a `type`, an
51
51
  `is_unique` flag, and — load-bearing — a `privacy_requirement` that decides how
@@ -115,7 +115,7 @@ while (Date.now() < deadline && !defaultDacSlug) {
115
115
  if (!defaultDacSlug) await new Promise((r) => setTimeout(r, 1_000))
116
116
  }
117
117
  if (!defaultDacSlug) {
118
- throw new Error('default DAC for the custom table was not provisioned within 30s')
118
+ throw new Error('default DAC for the generic table was not provisioned within 30s')
119
119
  }
120
120
  ctx.defaultDacSlug = defaultDacSlug
121
121
  ```
@@ -144,34 +144,34 @@ if (!ingest.batch_id) {
144
144
  }
145
145
  ```
146
146
 
147
- ## 006 — read the row back by its unique key
147
+ ## 006 — read the row back with read-only SQL
148
148
 
149
- Search the `generic_table` dataset, scoped to this table via `generic_table_id`,
150
- filtering on the regulated table name (`regulated_<table_name>`) and the unique
151
- column. `createUserSearch` compiles the query; `search` returns rows once
152
- ingestion has landed — poll until the row appears.
149
+ `executeSql` runs a read-only statement against the datalake and returns a page
150
+ as `{ data, meta }` — `data` is an array-of-arrays aligned positionally to
151
+ `meta.columns`. `mode: 'unregulated'` runs it against the unregulated schema, so
152
+ you reference the table by its server-derived `name` (the `alvera_custom_…`
153
+ value from §002 — `mode` selects the schema, so no `regulated_`/`unregulated_`
154
+ prefix). Ingestion is async, so poll until the row lands.
153
155
 
154
156
  ```typescript
155
- const { data: userSearch } = await api.datasets.createUserSearch(tenantSlug, datalakeSlug, 'generic_table', {
156
- search_query: `regulated_${ctx.tableName}.submission_id = '${ctx.submissionId}'`,
157
- generic_table_id: genericTableId,
158
- })
159
- if (userSearch.status !== 'completed') {
160
- throw new Error(`user-search did not compile: ${userSearch.status}`)
161
- }
162
-
163
157
  const deadline = Date.now() + 45_000
164
- let rows: Array<Record<string, unknown>> = []
165
- while (Date.now() < deadline && rows.length === 0) {
166
- const { data: page } = await api.datasets.search(tenantSlug, datalakeSlug, 'generic_table', {
167
- userSearchId: userSearch.id!,
168
- dataAccessMode: 'unregulated',
158
+ let row: Record<string, unknown> | undefined
159
+ while (Date.now() < deadline && !row) {
160
+ const { data: result } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
161
+ sql: `SELECT submission_id, customer_name, source_channel
162
+ FROM ${ctx.tableName}
163
+ WHERE submission_id = '${ctx.submissionId}'`,
164
+ mode: 'unregulated',
169
165
  })
170
- rows = (page.data ?? []) as Array<Record<string, unknown>>
171
- if (rows.length === 0) await new Promise((r) => setTimeout(r, 1_000))
166
+ // JSON envelope (a `string` only for `{ format: 'csv' }`); zip the positional
167
+ // row back into an object keyed by meta.columns.
168
+ if (typeof result !== 'string' && result.data.length > 0) {
169
+ row = Object.fromEntries(result.meta.columns.map((c, i) => [c, result.data[0][i]]))
170
+ }
171
+ if (!row) await new Promise((r) => setTimeout(r, 1_000))
172
172
  }
173
- if (!rows.some((r) => r.submission_id === ctx.submissionId)) {
174
- throw new Error('ingested row not found in the custom table within 45s')
173
+ if (!row || row.submission_id !== ctx.submissionId) {
174
+ throw new Error('ingested row not found in the generic table within 45s')
175
175
  }
176
176
  ```
177
177
 
@@ -182,20 +182,23 @@ if (!rows.some((r) => r.submission_id === ctx.submissionId)) {
182
182
  free text is scrubbed, `none` passes through. Dropping it silently changes who
183
183
  can see what.
184
184
  - **The table name is server-derived, never set by you.** The platform slugifies
185
- the title and prefixes `alvera_custom_`. Read `name` off the create response;
186
- the search query uses `regulated_<name>`.
185
+ the title and prefixes `alvera_custom_`. Read `name` off the create response and
186
+ reference it directly in `executeSql`; `mode` selects the schema, so you don't
187
+ add a `regulated_`/`unregulated_` prefix yourself.
187
188
  - **You don't create the ingestion client.** Deploying the table auto-provisions
188
189
  a default DAC + identity contract. Find it by name; don't build one.
189
- - **Two async waits.** Wait for `:deployed` before ingesting, then poll the
190
- search until the row materializes — ingestion is async like every other
190
+ - **Two async waits.** Wait for `:deployed` before ingesting, then poll
191
+ `executeSql` until the row materializes — ingestion is async like every other
191
192
  ingest path.
192
- - **`generic_table_id` on the search is required.** It routes the query to your
193
- table's dataset; omitting it searches the wrong scope.
193
+ - **`mode` picks the schema, and `tokenize` columns come back masked.** In
194
+ `mode: 'unregulated'` the read runs against the tokenized view, so filter and
195
+ assert on a `none` column (here `submission_id`); a `tokenize` column like
196
+ `customer_name` returns its token, not the raw value.
194
197
 
195
198
  # See also
196
199
 
197
200
  - `generic_tables.md` — column types, `privacy_requirement`, lifecycle reference
198
201
  - `data_activation_clients.md` — the `.ingest` runtime verb + dataset search
199
- - `_setup/accounts_receivable.md` — the bootstrap this walk starts from
200
- - `integration-tests/tests/accounts_receivable/generic-tables.test.ts` — the
202
+ - `_setup/subscription.md` — the bootstrap this walk starts from
203
+ - `integration-tests/tests/subscription/generic-tables.test.ts` — the
201
204
  green test these calls are lifted from
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: "Capability: invite a teammate into your tenant"
3
3
  summary: A capability walk for multi-user onboarding. An admin invites a colleague by email (`api.invitations.create`), the colleague signs up and is confirmed, signs in without a tenant to find the pending invite (`invitations.list`), accepts it (`invitations.accept`), and finally signs in tenant-scoped as a member. Shows the three session scopes — root, tenantless, tenant-scoped — in one flow.
4
- industry: accounts_receivable
4
+ industry: subscription
5
5
  slug: invite-team
6
6
  vitest_source:
7
- - integration-tests/tests/accounts_receivable/invite-team.test.ts
8
- - integration-tests/tests/accounts_receivable/bootstrap.test.ts
7
+ - integration-tests/tests/subscription/invite-team.test.ts
8
+ - integration-tests/tests/subscription/bootstrap.test.ts
9
9
  status: green
10
10
  ---
11
11
 
@@ -18,7 +18,7 @@ The flow touches all three session scopes (see `AGENTS.md` § SDK auth):
18
18
 
19
19
  - **tenant-scoped** (the admin) — `api.invitations.create(tenantSlug, { email, role })`
20
20
  - **root** — signs the new user up and confirms them
21
- (`api.auth.signUp` + `api.admin.confirmUser`)
21
+ (`api.admin.signUp` + `api.admin.confirmUser`)
22
22
  - **tenantless** (the invitee, before they belong anywhere) — lists pending
23
23
  invites and accepts one (`api.invitations.list` / `api.invitations.accept`)
24
24
 
@@ -27,7 +27,7 @@ This capability is **global** — it works the same on every datalake's tenant.
27
27
 
28
28
  # Walkthrough
29
29
 
30
- The `_setup/accounts_receivable.md` bootstrap already provisioned the tenant and
30
+ The `_setup/subscription.md` bootstrap already provisioned the tenant and
31
31
  left `api` as the admin's tenant-scoped client, with `tenantSlug` and
32
32
  `ctx.sarahEmail` / `ctx.sarahPassword` populated. Root credentials come from the
33
33
  `ALVERA_ROOT_EMAIL` / `ALVERA_ROOT_PASSWORD` env vars the validator already
@@ -83,7 +83,7 @@ root-scoped actions, so build a root client from the `ALVERA_ROOT_*` credentials
83
83
  and use it. `confirmUser` activates the account so the invitee can sign in.
84
84
 
85
85
  ```typescript
86
- const rootSession = await createSession({
86
+ const rootSession = await createBootstrapSession({
87
87
  baseUrl: process.env.ALVERA_BASE_URL!,
88
88
  email: process.env.ALVERA_ROOT_EMAIL!,
89
89
  password: process.env.ALVERA_ROOT_PASSWORD!,
@@ -91,9 +91,10 @@ const rootSession = await createSession({
91
91
  const rootApi = createIsolatedPlatformApi({
92
92
  baseUrl: process.env.ALVERA_BASE_URL!,
93
93
  sessionToken: rootSession.sessionToken,
94
+ apiKey: '',
94
95
  })
95
96
 
96
- const { data: emmaUser } = await rootApi.auth.signUp({
97
+ const { data: emmaUser } = await rootApi.admin.signUp({
97
98
  email: ctx.emmaEmail,
98
99
  password: ctx.emmaPassword,
99
100
  first_name: 'Emma',
@@ -105,16 +106,16 @@ await rootApi.admin.confirmUser(emmaUser.id!)
105
106
  ## 004 — teammate signs in WITHOUT a tenant and finds the invite
106
107
 
107
108
  The invitee has an account but belongs to no tenant yet, so they sign in
108
- **tenantless** — `createSession` with no `tenantSlug`. The resulting session has
109
+ **tenantless** — `createBootstrapSession` (`createSession` is tenant login
110
+ only since the universal-key contract). The resulting session has
109
111
  `tenant: null`. A tenantless client can list the invitations waiting for that
110
112
  user. Find the one for this tenant by matching `tenant.slug`.
111
113
 
112
114
  ```typescript
113
- const emmaTenantless = await createSession({
115
+ const emmaTenantless = await createBootstrapSession({
114
116
  baseUrl: process.env.ALVERA_BASE_URL!,
115
117
  email: ctx.emmaEmail,
116
118
  password: ctx.emmaPassword,
117
- // no tenantSlug → tenantless session
118
119
  })
119
120
  if (emmaTenantless.tenant !== null) {
120
121
  throw new Error('expected a tenantless session (tenant should be null)')
@@ -122,6 +123,7 @@ if (emmaTenantless.tenant !== null) {
122
123
  const emmaTenantlessApi = createIsolatedPlatformApi({
123
124
  baseUrl: process.env.ALVERA_BASE_URL!,
124
125
  sessionToken: emmaTenantless.sessionToken,
126
+ apiKey: '',
125
127
  })
126
128
  ctx.emmaTenantlessToken = emmaTenantless.sessionToken
127
129
 
@@ -142,6 +144,7 @@ the new `role` and the `tenant.slug` the user just joined.
142
144
  const emmaTenantlessApi = createIsolatedPlatformApi({
143
145
  baseUrl: process.env.ALVERA_BASE_URL!,
144
146
  sessionToken: ctx.emmaTenantlessToken,
147
+ apiKey: '',
145
148
  })
146
149
 
147
150
  const { data: membership } = await emmaTenantlessApi.invitations.accept(ctx.invitationId)
@@ -153,7 +156,9 @@ if (membership.tenant?.slug !== tenantSlug || membership.role !== 'member') {
153
156
  ## 006 — teammate signs in tenant-scoped as a member
154
157
 
155
158
  Now that the membership exists, the invitee re-authenticates **with** the
156
- tenant slug. The new session carries `tenant.slug` and a `member` role — they
159
+ tenant slug — which requires the tenant's API key (`ctx.tenantApiKey`, minted
160
+ during setup): tenant-scoped sign-in is 401'd without a resolvable
161
+ `X-API-Key`. The new session carries `tenant.slug` and a `member` role — they
157
162
  are in.
158
163
 
159
164
  ```typescript
@@ -162,6 +167,7 @@ const emmaScoped = await createSession({
162
167
  email: ctx.emmaEmail,
163
168
  password: ctx.emmaPassword,
164
169
  tenantSlug,
170
+ apiKey: ctx.tenantApiKey,
165
171
  })
166
172
  if (emmaScoped.tenant?.slug !== tenantSlug) {
167
173
  throw new Error(`expected a tenant-scoped session on ${tenantSlug}`)
@@ -189,6 +195,6 @@ if (!/member/i.test(emmaScoped.role?.name ?? '')) {
189
195
 
190
196
  - `AGENTS.md` § SDK auth + client construction — root / tenantless / tenant-scoped
191
197
  - `account_management.md` — users, roles, invitations reference
192
- - `_setup/accounts_receivable.md` — the bootstrap this walk starts from
193
- - `integration-tests/tests/accounts_receivable/invite-team.test.ts` — the green
198
+ - `_setup/subscription.md` — the bootstrap this walk starts from
199
+ - `integration-tests/tests/subscription/invite-team.test.ts` — the green
194
200
  test these calls are lifted from
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  title: Send a KYC-notification SMS when a payment account becomes active
3
- summary: End-to-end standard workflow — provision a KYC-notification workflow over the payment-risk payment_account dataset, ingest two Atomic FI payment-account rows (one active, one suspended) through a payment_account interop contract, run the workflow so the status filter routes each row, read the rendered SMS back from the message dataset, and close the connected-app reply loop. No agent, no LLM.
4
- industry: payment_risk
3
+ summary: End-to-end standard workflow — provision a KYC-notification workflow over the payments payment_account dataset, ingest two Atomic FI payment-account rows (one active, one suspended) through a payment_account interop contract, run the workflow so the status filter routes each row, read the rendered SMS back from the message dataset, and close the connected-app reply loop. No agent, no LLM.
4
+ industry: payments
5
5
  slug: kyc-notification-on-account-activation
6
6
  vitest_source:
7
- - integration-tests/tests/payment_risk/standard-workflow.test.ts
8
- - integration-tests/tests/payment_risk/interoperability-contracts.test.ts
9
- - integration-tests/tests/payment_risk/run-dac-single.test.ts
10
- - integration-tests/tests/payment_risk/create-dac.test.ts
11
- - integration-tests/tests/payment_risk/tools.test.ts
12
- - integration-tests/tests/payment_risk/data-sources.test.ts
13
- - integration-tests/tests/payment_risk/bootstrap.test.ts
7
+ - integration-tests/tests/payments/standard-workflow.test.ts
8
+ - integration-tests/tests/payments/interoperability-contracts.test.ts
9
+ - integration-tests/tests/payments/run-dac-single.test.ts
10
+ - integration-tests/tests/payments/create-dac.test.ts
11
+ - integration-tests/tests/payments/tools.test.ts
12
+ - integration-tests/tests/payments/data-sources.test.ts
13
+ - integration-tests/tests/payments/bootstrap.test.ts
14
14
  status: green
15
15
  ---
16
16
 
@@ -28,7 +28,7 @@ active ones to an SMS sender — with the status gate living in
28
28
  application code.
29
29
 
30
30
  The Alvera platform's standard workflow primitive keeps that gate
31
- inside the platform. A payment-risk `payment_account` dataset is
31
+ inside the platform. A payments `payment_account` dataset is
32
32
  the workflow's event source; a pure-Liquid filter gates on
33
33
  `payment_account.status == "active"`; a single SMS action carries
34
34
  a deep-link to a connected-app KYC portal. The
@@ -43,10 +43,10 @@ the message dataset, and finally resolves the connected-app
43
43
  deep-link the SMS carries — closing the SMS → reply loop.
44
44
 
45
45
  The scenario is anchored to
46
- `platform/integration-tests/tests/payment_risk/standard-workflow.test.ts`
46
+ `platform/integration-tests/tests/payments/standard-workflow.test.ts`
47
47
  — a green end-to-end test (§1–§7) that exercises this exact shape.
48
48
  This cookbook is a prose re-presentation of what that test walks.
49
- The setup file `_setup/payment_risk.md` already provisioned the
49
+ The setup file `_setup/payments.md` already provisioned the
50
50
  tenant, datalake, and industry-admin client; this cookbook starts
51
51
  from there.
52
52
 
@@ -62,7 +62,7 @@ from there.
62
62
  | Atomic FI Payment Account interop contract | build |
63
63
  | Manual-upload DAC | build |
64
64
 
65
- The setup file `_setup/payment_risk.md` already provisioned the
65
+ The setup file `_setup/payments.md` already provisioned the
66
66
  tenant + datalake + tenant-scoped client; this cookbook starts
67
67
  from there.
68
68
 
@@ -133,7 +133,7 @@ so the recipient knows which account activated, and
133
133
  `{{ connected_app_form_url }}` so the platform can bake in the
134
134
  `/t/<token>` KYC-portal link.
135
135
 
136
- `dataset_type: 'payment_account'` makes the payment-risk payment
136
+ `dataset_type: 'payment_account'` makes the payments payment
137
137
  account dataset the event source; the workflow context injects
138
138
  the payment account row under the `payment_account` key, so the
139
139
  filter reads `payment_account.status` directly. A
@@ -142,7 +142,7 @@ KYC-notification SMS already sent to the same legal entity in the
142
142
  last six months — the platform's built-in guard against
143
143
  re-notifying. On a fresh tenant that lookup returns empty, which
144
144
  is harmless. The SMS `to` is a fixed inbox number because a
145
- payment-risk legal entity's contact fields are tokenized and not
145
+ payments legal entity's contact fields are tokenized and not
146
146
  resolved into the SMS templating context here.
147
147
 
148
148
  ```typescript
@@ -250,7 +250,7 @@ ctx.manualUploadToolId = manualUploadToolResp.data.id!
250
250
 
251
251
  The interoperability contract is the row-shaping rule: a Liquid
252
252
  template that maps an inbound Atomic FI payment-account row into a
253
- payment-risk `PaymentAccount` upsert. This cookbook loads the
253
+ payments `PaymentAccount` upsert. This cookbook loads the
254
254
  production Atomic FI payment-account + MDM templates from the
255
255
  vendored fixtures directory rather than inlining the Liquid.
256
256
 
@@ -266,17 +266,17 @@ to.
266
266
  const { readFileSync } = await import('node:fs')
267
267
  const { join } = await import('node:path')
268
268
  const paTemplate = readFileSync(
269
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'payment_risk/_payment_accounts_payment_risk_payment_account.liquid'),
269
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'payments/_payment_accounts_payments_payment_account.liquid'),
270
270
  'utf8',
271
271
  )
272
272
  const mdmTemplate = readFileSync(
273
- join(process.env.COOKBOOK_FIXTURES_DIR!, 'payment_risk/_payment_accounts_payment_risk_mdm.liquid'),
273
+ join(process.env.COOKBOOK_FIXTURES_DIR!, 'payments/_payment_accounts_payments_mdm.liquid'),
274
274
  'utf8',
275
275
  )
276
276
 
277
277
  const contractResp = await api.interoperabilityContracts.create(tenantSlug, datalakeSlug, {
278
278
  name: `Cookbook Atomic FI Payment Account Contract ${runSuffix}`,
279
- description: 'Atomic FI payment-accounts → PaymentRisk PaymentAccount (custom Liquid + MDM input).',
279
+ description: 'Atomic FI payment-accounts → Payments PaymentAccount (custom Liquid + MDM input).',
280
280
  resource_type: 'payment_account',
281
281
  template_config: { type: 'custom', body: paTemplate },
282
282
  mdm_input_config: { type: 'custom', body: mdmTemplate },
@@ -559,7 +559,7 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
559
559
  # Rollback
560
560
 
561
561
  The cookbook doctest harness does not currently tear down created
562
- resources. The `_setup/payment_risk.md` setup file's
562
+ resources. The `_setup/payments.md` setup file's
563
563
  runSuffix-scoped tenant / datalake / user names mean each run is
564
564
  naturally isolated; the seeded local DB is cheap to reset
565
565
  (`mix ecto.reset` on the platform repo).
@@ -568,7 +568,7 @@ naturally isolated; the seeded local DB is cheap to reset
568
568
 
569
569
  After this cookbook's thirteen steps run green:
570
570
 
571
- - A payment-risk tenant exists with a payment-risk-domain
571
+ - A payments tenant exists with a payments-domain
572
572
  datalake
573
573
  - An SMS tool, a KYC-portal connected app, and a KYC-notification
574
574
  standard workflow (status `live`, `payment_account` dataset)
@@ -594,10 +594,10 @@ demonstrated end-to-end, not merely provisioned.
594
594
 
595
595
  # See also
596
596
 
597
- - `_setup/payment_risk.md` — the inlined bootstrap that
597
+ - `_setup/payments.md` — the inlined bootstrap that
598
598
  provisions the tenant + datalake this cookbook starts from
599
599
  - `sanctions-screening-with-agent-review.md` — the agent-driven
600
- payment-risk cookbook; an LLM agent disambiguates gray-zone
600
+ payments cookbook; an LLM agent disambiguates gray-zone
601
601
  sanctions screenings
602
602
  - `.agent/tools.md` — SMS (`tool_body_type: sns`) and
603
603
  manual-upload tool body shapes; intent classification
@@ -609,11 +609,11 @@ demonstrated end-to-end, not merely provisioned.
609
609
  interop contract → DAC ingestion chain
610
610
  - `.agent/workflows.md` — standard workflow primitive (filter +
611
611
  decision + actions), `context_datasets`, `workflows.run`
612
- - `.agent/cookbook/_fixtures/payment_risk/` — the vendored Atomic
612
+ - `.agent/cookbook/_fixtures/payments/` — the vendored Atomic
613
613
  FI payment-account / MDM Liquid templates §006 loads
614
- - `integration-tests/tests/payment_risk/standard-workflow.test.ts` —
614
+ - `integration-tests/tests/payments/standard-workflow.test.ts` —
615
615
  the anchor green test (§1–§7) these snippets are lifted from
616
- - `integration-tests/tests/payment_risk/interoperability-contracts.test.ts`,
616
+ - `integration-tests/tests/payments/interoperability-contracts.test.ts`,
617
617
  `run-dac-single.test.ts`, `create-dac.test.ts`, `tools.test.ts`,
618
618
  `data-sources.test.ts` — the per-resource create + ingest
619
619
  snippets §004–§009 are lifted from