@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,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:
|
|
4
|
+
industry: subscription
|
|
5
5
|
slug: dunning-sms-for-delinquent
|
|
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/
|
|
13
|
-
- integration-tests/tests/
|
|
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/
|
|
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/
|
|
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/
|
|
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!, '
|
|
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!, '
|
|
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 →
|
|
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/
|
|
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
|
|
574
|
-
|
|
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/
|
|
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/
|
|
612
|
+
- `.agent/cookbook/_fixtures/subscription/` — the vendored
|
|
613
613
|
Stripe customer / MDM Liquid templates §006 loads
|
|
614
|
-
- `integration-tests/tests/
|
|
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/
|
|
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
|
|
3
|
-
summary: A capability walk for generic tables. Create a
|
|
4
|
-
industry:
|
|
5
|
-
slug:
|
|
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/
|
|
8
|
-
- integration-tests/tests/
|
|
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. `
|
|
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/
|
|
33
|
+
The `_setup/subscription.md` bootstrap left `api`, `tenantSlug`, and
|
|
34
34
|
`datalakeSlug` populated.
|
|
35
35
|
|
|
36
|
-
## 001 — list existing
|
|
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
|
|
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
|
|
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
|
|
147
|
+
## 006 — read the row back with read-only SQL
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
165
|
-
while (Date.now() < deadline &&
|
|
166
|
-
const { data:
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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 (!
|
|
174
|
-
throw new Error('ingested row not found in the
|
|
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
|
|
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
|
|
190
|
-
|
|
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
|
-
- **`
|
|
193
|
-
|
|
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/
|
|
200
|
-
- `integration-tests/tests/
|
|
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:
|
|
4
|
+
industry: subscription
|
|
5
5
|
slug: invite-team
|
|
6
6
|
vitest_source:
|
|
7
|
-
- integration-tests/tests/
|
|
8
|
-
- integration-tests/tests/
|
|
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.
|
|
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/
|
|
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
|
|
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.
|
|
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** — `
|
|
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
|
|
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
|
|
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/
|
|
193
|
-
- `integration-tests/tests/
|
|
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
|
|
4
|
-
industry:
|
|
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/
|
|
8
|
-
- integration-tests/tests/
|
|
9
|
-
- integration-tests/tests/
|
|
10
|
-
- integration-tests/tests/
|
|
11
|
-
- integration-tests/tests/
|
|
12
|
-
- integration-tests/tests/
|
|
13
|
-
- integration-tests/tests/
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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!, '
|
|
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!, '
|
|
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 →
|
|
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/
|
|
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
|
|
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/
|
|
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
|
-
|
|
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/
|
|
612
|
+
- `.agent/cookbook/_fixtures/payments/` — the vendored Atomic
|
|
613
613
|
FI payment-account / MDM Liquid templates §006 loads
|
|
614
|
-
- `integration-tests/tests/
|
|
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/
|
|
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
|