@alvera-ai/platform-sdk 0.17.0 → 0.18.0
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 +82 -144
- package/.agent/account_management.md +2 -2
- package/.agent/action_logs.md +4 -4
- package/.agent/ai_agents.md +28 -21
- package/.agent/ai_sandbox.md +49 -39
- package/.agent/connected_apps.md +3 -3
- package/.agent/cookbook/_fixtures/README.md +1 -1
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
- package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
- package/.agent/cookbook/organic-marketing.md +2801 -0
- package/.agent/cookbook/payments-compliance.md +2180 -0
- package/.agent/cookbook/primary-care.md +2175 -0
- package/.agent/cookbook/subscription-saas.md +2403 -0
- package/.agent/data_activation_clients.md +65 -52
- package/.agent/datalakes.md +338 -171
- package/.agent/errors.md +3 -3
- package/.agent/generic_tables.md +151 -62
- package/.agent/interoperability_contracts.md +57 -22
- package/.agent/mdm.md +136 -153
- package/.agent/messages.md +36 -34
- package/.agent/mock-services.md +1 -1
- package/.agent/mutations.md +2 -2
- package/.agent/templates.md +14 -13
- package/.agent/tools.md +63 -21
- package/.agent/type_naming.md +13 -13
- package/.agent/workflows.md +99 -53
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +33 -47
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +565 -379
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +494 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
- package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
- package/.agent/cookbook/_setup/foundation.md +0 -359
- package/.agent/cookbook/_setup/healthcare.md +0 -361
- package/.agent/cookbook/_setup/payments.md +0 -365
- package/.agent/cookbook/_setup/subscription.md +0 -364
- package/.agent/cookbook/action-status-updaters.md +0 -278
- package/.agent/cookbook/ai-agent-invoke.md +0 -279
- package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
- package/.agent/cookbook/bulk-ingest.md +0 -302
- package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
- package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
- package/.agent/cookbook/generic-tables.md +0 -244
- package/.agent/cookbook/invite-team.md +0 -200
- package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
- package/.agent/cookbook/marketing-campaign-send.md +0 -1044
- package/.agent/cookbook/paginated-restapi-poller.md +0 -383
- package/.agent/cookbook/rest-fetch.md +0 -273
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
- package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
- package/.agent/cookbook/system-templates.md +0 -165
- package/.agent/cookbook/talk-to-data.md +0 -178
- package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
- package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Foundation Airflow Lead Form → Foundation LegalEntity attrs.
|
|
3
|
+
|
|
4
|
+
Maps a single lead-form row into a `LegalEntity` upsert payload. Used by
|
|
5
|
+
a contract whose `resource_type` is `"legal_entity"` and whose
|
|
6
|
+
`mdm_input_config` is `{ type: "null" }` — the row IS the subject, so
|
|
7
|
+
there is nothing to resolve and the legal entity is written directly.
|
|
8
|
+
Identifier dedupe in `Platform.Datasets.LegalEntities.upsert_legal_entity/5`
|
|
9
|
+
collapses repeat submissions sharing the same digital identifier (the
|
|
10
|
+
verified email) onto one legal-entity row.
|
|
11
|
+
|
|
12
|
+
ONE WRITER PER SUBJECT. That dedupe is a find-or-create against a unique
|
|
13
|
+
index on `(uri, id_type, id_number)` that deliberately omits the legal
|
|
14
|
+
entity, so two contracts resolving the same subject at the same instant
|
|
15
|
+
do not each succeed — the loser is refused. This template is safe on its
|
|
16
|
+
own client; pair it with a generic-table contract and they must be
|
|
17
|
+
sequenced, never bound to one client together.
|
|
18
|
+
|
|
19
|
+
## Branches
|
|
20
|
+
* `legal_entity_type: "individual"` when `company` is empty
|
|
21
|
+
* `legal_entity_type: "business"` when `company` is non-empty
|
|
22
|
+
(the company name becomes `business_name` and the submitter's name
|
|
23
|
+
is dropped — businesses don't carry first/last on the LE row)
|
|
24
|
+
|
|
25
|
+
## Identifications
|
|
26
|
+
* One identification per row: `id_type: "digital_identifier"`,
|
|
27
|
+
`uri: <p.source_uri>` — the system-of-record URI, which the Data
|
|
28
|
+
Activation Client injects from the configured `data_source.uri` —
|
|
29
|
+
and `id_number: <email>`. There is one schema and it holds the real
|
|
30
|
+
value; masking is infrastructure (the tokenized and redacted lakes),
|
|
31
|
+
not a second set of attrs to write into.
|
|
32
|
+
|
|
33
|
+
* NOTE the key names. A LegalEntity identification is
|
|
34
|
+
`id_type` / `id_number` / `uri`, which is what this template writes.
|
|
35
|
+
An MDMInput identifier — what a contract's `mdm_input_config`
|
|
36
|
+
renders — is `uri` / `type` / `value`. Unknown keys are dropped
|
|
37
|
+
rather than refused, so the wrong shape resolves nothing and fails
|
|
38
|
+
as a bare `mdm_dispatcher_error` naming no field.
|
|
39
|
+
{% endcomment %}
|
|
40
|
+
{% assign p = msg %}
|
|
41
|
+
{% assign le_type = "individual" %}
|
|
42
|
+
{% if p.company and p.company != "" %}{% assign le_type = "business" %}{% endif %}
|
|
43
|
+
{% assign name_parts = p.name | default: "" | split: " " %}
|
|
44
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
45
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
46
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
47
|
+
{
|
|
48
|
+
"legal_entity_type": "{{ le_type }}",
|
|
49
|
+
"role": "direct",
|
|
50
|
+
{% if le_type == "business" %}"business_name": "{{ p.company | json_escape }}",{% endif %}
|
|
51
|
+
{% if le_type == "individual" and first_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
52
|
+
{% if le_type == "individual" and last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% endif %}
|
|
53
|
+
{% comment %}
|
|
54
|
+
Optional date_of_birth + phone_numbers, flat like every other
|
|
55
|
+
field. There is ONE LegalEntity schema now and it declares both —
|
|
56
|
+
`field(:date_of_birth, MaskedDate)` and
|
|
57
|
+
`has_many(:phone_numbers, LegalEntityPhoneNumber)`. GH-859 removed
|
|
58
|
+
the public/regulated pair of schemas, and with it the nested
|
|
59
|
+
`regulated_legal_entity` block this template used to carry:
|
|
60
|
+
masking is infrastructure — the tokenized and redacted lakes —
|
|
61
|
+
not a second schema to write into. A column holds its real value.
|
|
62
|
+
|
|
63
|
+
`date_of_birth` is rendered as ISO 8601 YYYY-MM-DD; if the CSV
|
|
64
|
+
column is absent, the {% if %} guard renders nothing and
|
|
65
|
+
:date_of_birth stays nil.
|
|
66
|
+
|
|
67
|
+
`phone_numbers` renders as a single-element list — matches the
|
|
68
|
+
one-phone-per-lead shape of inbound lead-capture forms. Absent
|
|
69
|
+
column leaves the has_many empty.
|
|
70
|
+
{% endcomment %}
|
|
71
|
+
{% if p.date_of_birth and p.date_of_birth != "" %}"date_of_birth": "{{ p.date_of_birth | json_escape }}",{% endif %}
|
|
72
|
+
{% if p.phone and p.phone != "" %}"phone_numbers": [{ "phone_number": "{{ p.phone | json_escape }}" }],{% endif %}
|
|
73
|
+
"identifications": [
|
|
74
|
+
{
|
|
75
|
+
"id_type": "digital_identifier",
|
|
76
|
+
"uri": "{{ p.source_uri | json_escape }}",
|
|
77
|
+
"id_number": "{{ p.email | json_escape }}"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
whose `find_legal_entity_by_identifications/2` dedupe path collapses
|
|
14
14
|
repeat resolves sharing the same `(id_type, id_number, uri)` triple
|
|
15
15
|
onto the SAME LegalEntity row written by Contract A.
|
|
16
|
-
3. Pins the resulting GT event's `
|
|
16
|
+
3. Pins the resulting GT event's `legal_entity_id == legal_entity.id`.
|
|
17
17
|
|
|
18
18
|
The verifying platform URI flows from `data_source.uri` injected by DAC
|
|
19
19
|
as `source_uri` — same upstream channel as Contract A's LE template, so
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"identifiers": [
|
|
43
43
|
{
|
|
44
44
|
"system": "{{ p.source_uri | json_escape }}",
|
|
45
|
+
"type": "digital_identifier",
|
|
45
46
|
"value": "{{ p.email | json_escape }}"
|
|
46
47
|
}
|
|
47
48
|
]
|
package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI ComplianceScreenings → `compliance_screenings` generic_table row.
|
|
3
|
+
|
|
4
|
+
Contract B (`resource_type: "generic_table"`, `generic_table_id` pinned
|
|
5
|
+
to the compliance-screenings table).
|
|
6
|
+
|
|
7
|
+
Source: GET /api/compliance-screenings on Atomic FI (paginated; the
|
|
8
|
+
fetch pipeline extracts the `data` array via the response_extractor and
|
|
9
|
+
streams one row per `msg`).
|
|
10
|
+
|
|
11
|
+
GH-859 removed the `compliance_screening` resource, and with it the
|
|
12
|
+
changeset that used to `put_change` the FK columns and call
|
|
13
|
+
`cast_tokenized_data` on the way past. Both jobs move to declarations:
|
|
14
|
+
`legal_entity_id` is stamped by the platform from Contract B's MDM
|
|
15
|
+
resolve (never a column, never written here), and the narrative + name
|
|
16
|
+
fields are `privacy_requirement: "tokenize"` / `"redact"` on the table,
|
|
17
|
+
so the raw value goes in and the derived lakes do the masking.
|
|
18
|
+
|
|
19
|
+
Two things survive unchanged, because they were always this template's
|
|
20
|
+
own work:
|
|
21
|
+
|
|
22
|
+
* The analytic numerics (`screening_score`, `aml_risk_score`,
|
|
23
|
+
`aml_velocity_count`) render UNQUOTED so they land as real numbers.
|
|
24
|
+
The workflow's gray-zone filter compares `screening_score` with
|
|
25
|
+
`>=` and `<`; a quoted "0.88" would compare as a string and the
|
|
26
|
+
band would silently stop meaning anything.
|
|
27
|
+
* `sanctions_matches` is explicitly looped. Liquid's default
|
|
28
|
+
rendering of a list produces a stringified Elixir list, not JSON —
|
|
29
|
+
the `{% for %}` emits one object per element so the jsonb column
|
|
30
|
+
receives a real array.
|
|
31
|
+
{% endcomment %}
|
|
32
|
+
{% assign p = msg %}
|
|
33
|
+
{
|
|
34
|
+
"compliance_screening_number": "{{ p.compliance_screening_number | default: p.id | json_escape }}",
|
|
35
|
+
"account_holder_ref": "{{ p.account_holder_id | default: "" | json_escape }}",
|
|
36
|
+
"scope": "{{ p.scope | default: "account_holder" | json_escape }}",
|
|
37
|
+
"screening_type": "{{ p.screening_type | default: "sanctions" | json_escape }}",
|
|
38
|
+
"screening_status": "{{ p.screening_status | default: "pending" | json_escape }}",
|
|
39
|
+
"screened_entity_type": "{{ p.screened_entity_type | default: "individual" | json_escape }}",
|
|
40
|
+
"screening_score": {{ p.screening_score | default: 0 }},
|
|
41
|
+
"match_count": {{ p.match_count | default: 0 }},
|
|
42
|
+
"sanctions_screening_status": "{{ p.sanctions_screening_status | default: "" | json_escape }}",
|
|
43
|
+
"screened_entity_name": "{{ p.screened_entity_name | default: "" | json_escape }}",
|
|
44
|
+
"review_notes": "{{ p.review_notes | default: "" | json_escape }}",
|
|
45
|
+
"pep_list_name": "{{ p.pep_list_name | default: "" | json_escape }}",
|
|
46
|
+
"aml_risk_score": {{ p.aml_risk_score | default: 0 }},
|
|
47
|
+
"aml_velocity_count": {{ p.aml_velocity_count | default: 0 }},
|
|
48
|
+
"aml_high_risk_country": "{{ p.aml_high_risk_country | default: "" | json_escape }}",
|
|
49
|
+
"sanctions_matches": [{% for m in p.sanctions_matches %}{
|
|
50
|
+
"matched_name": "{{ m.matched_name | json_escape }}",
|
|
51
|
+
"matched_entity_type": "{{ m.matched_entity_type | default: "" | json_escape }}",
|
|
52
|
+
"match_score": {{ m.match_score | default: 0 }},
|
|
53
|
+
"source_list": "{{ m.source_list | default: "" | json_escape }}",
|
|
54
|
+
"source_id": "{{ m.source_id | default: "" | json_escape }}",
|
|
55
|
+
"false_positive_qualifier": "{{ m.false_positive_qualifier | default: "" | json_escape }}"
|
|
56
|
+
}{% unless forloop.last %},{% endunless %}{% endfor %}]
|
|
57
|
+
}
|
package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI ComplianceScreenings → LegalEntity attrs.
|
|
3
|
+
|
|
4
|
+
Contract A (`resource_type: "legal_entity"`, `mdm_input_config`
|
|
5
|
+
`{ type: "null" }`). The account HOLDER a screening was run against is
|
|
6
|
+
the MDM subject, so this template writes that legal entity directly,
|
|
7
|
+
keyed on Atomic FI's `account_holder_id`.
|
|
8
|
+
|
|
9
|
+
Contract B's MDM input emits the SAME `(uri, account_holder_id)` pair,
|
|
10
|
+
which is what makes both contracts converge on one legal entity per
|
|
11
|
+
account holder rather than two.
|
|
12
|
+
{% endcomment %}
|
|
13
|
+
{% assign p = msg %}
|
|
14
|
+
{% assign name_parts = p.screened_entity_name | default: "" | split: " " %}
|
|
15
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
16
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
17
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
18
|
+
{
|
|
19
|
+
"legal_entity_type": "individual",
|
|
20
|
+
"role": "direct",
|
|
21
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
22
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.account_holder_id | json_escape }}",{% endif %}
|
|
23
|
+
"identifications": [
|
|
24
|
+
{
|
|
25
|
+
"id_type": "digital_identifier",
|
|
26
|
+
"uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
|
|
27
|
+
"id_number": "{{ p.account_holder_id | json_escape }}"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI ComplianceScreenings → MDMInput JSON.
|
|
3
|
+
|
|
4
|
+
Used by Contract B's `mdm_input_config` (`resource_type:
|
|
5
|
+
"generic_table"`). A screening references an account-holder id
|
|
6
|
+
(`p.account_holder_id`); resolving it routes through the LegalEntity
|
|
7
|
+
upsert, whose identification dedupe collapses repeat resolves sharing
|
|
8
|
+
the same `(id_type, id_number, uri)` triple onto the SAME LegalEntity
|
|
9
|
+
row Contract A wrote. The resolved subject's id is then stamped onto
|
|
10
|
+
the screening row as `legal_entity_id`.
|
|
11
|
+
|
|
12
|
+
GH-859 removed the `owner_type` / `account_holder_*` vocabulary along
|
|
13
|
+
with the AccountHolder resource: there is no LE+AH parent pair to
|
|
14
|
+
find-or-create, just the legal entity. The identifier `uri` flows from
|
|
15
|
+
`data_source.uri` — injected by the DAC as `source_uri`, the same channel
|
|
16
|
+
Contract A reads — so both contracts converge on one subject.
|
|
17
|
+
|
|
18
|
+
NOTE the key names. An MDMInput identifier is `uri` / `type` / `value`. A
|
|
19
|
+
LegalEntity identification, which is what Contract A writes, is `id_type` /
|
|
20
|
+
`id_number` / `uri`. They are different shapes for the same idea, and
|
|
21
|
+
unknown keys are dropped rather than refused — so the wrong one resolves
|
|
22
|
+
nothing and fails as a bare `mdm_dispatcher_error` naming no field.
|
|
23
|
+
|
|
24
|
+
The screened entity's name is carried onto the subject when the export
|
|
25
|
+
supplies one, which is what lets a reviewer see who was screened
|
|
26
|
+
without joining back to the screening row.
|
|
27
|
+
{% endcomment %}
|
|
28
|
+
{% assign p = msg %}
|
|
29
|
+
{% assign name_parts = p.screened_entity_name | default: "" | split: " " %}
|
|
30
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
31
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
32
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
33
|
+
{
|
|
34
|
+
"legal_entity_type": "individual",
|
|
35
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
36
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.account_holder_id | json_escape }}",{% endif %}
|
|
37
|
+
"identifiers": [
|
|
38
|
+
{
|
|
39
|
+
"uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
|
|
40
|
+
"type": "digital_identifier",
|
|
41
|
+
"value": "{{ p.account_holder_id | json_escape }}"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI PaymentAccount → `payment_accounts` generic_table row.
|
|
3
|
+
|
|
4
|
+
Contract B (`resource_type: "generic_table"`, `generic_table_id` pinned
|
|
5
|
+
to the payment-accounts table). One JSON field per GT column.
|
|
6
|
+
|
|
7
|
+
Source: GET /api/payment-accounts on Atomic FI (paginated; the fetch
|
|
8
|
+
pipeline extracts the `data` array via the response_extractor and
|
|
9
|
+
streams one row per `msg`).
|
|
10
|
+
|
|
11
|
+
GH-859 removed the `payment_account` resource along with every other
|
|
12
|
+
per-domain dataset, so the changeset that used to `put_change` the FK
|
|
13
|
+
columns is gone with it. `legal_entity_id` arrives the way it does for
|
|
14
|
+
every generic table now: Contract B's `mdm_input_config` resolves the
|
|
15
|
+
account holder and the platform stamps the subject onto the row. It is
|
|
16
|
+
NEVER declared as a column and never written here.
|
|
17
|
+
|
|
18
|
+
What survives the conversion is the part that was always this
|
|
19
|
+
template's own work:
|
|
20
|
+
|
|
21
|
+
* PCI-DSS-sensitive fields (`account_number`, `iban`, `card_pan`,
|
|
22
|
+
`wallet_address`) and the free-text narratives are declared
|
|
23
|
+
`privacy_requirement: "tokenize"` / `"redact"` on the table, so
|
|
24
|
+
the raw value goes in here and the derived lakes do the masking.
|
|
25
|
+
Masking is infrastructure, not a second schema to write into.
|
|
26
|
+
* `destination_country` is derived from `swift_bic` (chars 5-6 per
|
|
27
|
+
ISO 9362) or `iban` (chars 1-2 per ISO 13616), BIC preferred when
|
|
28
|
+
both are present, so a downstream agent reads the field instead of
|
|
29
|
+
re-parsing the identifier.
|
|
30
|
+
* `enabled_regimes` is joined into one delimited string rather than
|
|
31
|
+
rendered as a JSON array — a generic-table column is scalar, and a
|
|
32
|
+
`{{ x }}` on a list would emit a stringified Elixir list anyway.
|
|
33
|
+
{% endcomment %}
|
|
34
|
+
{% assign p = msg %}
|
|
35
|
+
{% if p.swift_bic and p.swift_bic != "" %}
|
|
36
|
+
{% assign derived_country = p.swift_bic | slice: 4, 2 | upcase %}
|
|
37
|
+
{% elsif p.iban and p.iban != "" %}
|
|
38
|
+
{% assign derived_country = p.iban | slice: 0, 2 | upcase %}
|
|
39
|
+
{% else %}
|
|
40
|
+
{% assign derived_country = "" %}
|
|
41
|
+
{% endif %}
|
|
42
|
+
{
|
|
43
|
+
"payment_account_external_id": "{{ p.payment_account_external_id | default: "" | json_escape }}",
|
|
44
|
+
"payment_account_number": "{{ p.payment_account_number | default: "" | json_escape }}",
|
|
45
|
+
"account_holder_ref": "{{ p.account_holder_id | default: "" | json_escape }}",
|
|
46
|
+
"status": "{{ p.status | default: "pending" | json_escape }}",
|
|
47
|
+
"account_type": "{{ p.account_type | default: "bank_account" | json_escape }}",
|
|
48
|
+
"currency": "{{ p.currency | default: "" | json_escape }}",
|
|
49
|
+
"routing_number": "{{ p.routing_number | default: "" | json_escape }}",
|
|
50
|
+
"swift_bic": "{{ p.swift_bic | default: "" | json_escape }}",
|
|
51
|
+
"bank_name": "{{ p.bank_name | default: "" | json_escape }}",
|
|
52
|
+
"account_number": "{{ p.account_number | default: "" | json_escape }}",
|
|
53
|
+
"iban": "{{ p.iban | default: "" | json_escape }}",
|
|
54
|
+
"destination_country": "{{ derived_country | json_escape }}",
|
|
55
|
+
"purpose_of_payment_detail": "{{ p.purpose_of_payment_detail | default: "" | json_escape }}",
|
|
56
|
+
"enabled_regimes": "{% for r in p.enabled_regimes %}{{ r | json_escape }}{% unless forloop.last %},{% endunless %}{% endfor %}"
|
|
57
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI PaymentAccount → LegalEntity attrs.
|
|
3
|
+
|
|
4
|
+
Contract A (`resource_type: "legal_entity"`, `mdm_input_config`
|
|
5
|
+
`{ type: "null" }`). The account HOLDER is the MDM subject a payment
|
|
6
|
+
account belongs to, so this template writes that legal entity
|
|
7
|
+
directly, keyed on Atomic FI's `account_holder_id`.
|
|
8
|
+
|
|
9
|
+
GH-859 removed the per-domain identity chain: there is no
|
|
10
|
+
`account_holder` resource sitting between the payment account and its
|
|
11
|
+
legal entity, and no `owner_type` to choose. One LegalEntity schema
|
|
12
|
+
carries every subject, and a payment account is a generic table row
|
|
13
|
+
that points at one.
|
|
14
|
+
|
|
15
|
+
Contract B's MDM input emits the SAME `(uri, account_holder_id)` pair,
|
|
16
|
+
which is what makes both contracts converge on one legal entity per
|
|
17
|
+
account holder rather than two.
|
|
18
|
+
{% endcomment %}
|
|
19
|
+
{% assign p = msg %}
|
|
20
|
+
{% assign name_parts = p.account_holder_name | default: "" | split: " " %}
|
|
21
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
22
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
23
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
24
|
+
{
|
|
25
|
+
"legal_entity_type": "individual",
|
|
26
|
+
"role": "direct",
|
|
27
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
28
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.account_holder_id | json_escape }}",{% endif %}
|
|
29
|
+
"identifications": [
|
|
30
|
+
{
|
|
31
|
+
"id_type": "digital_identifier",
|
|
32
|
+
"uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
|
|
33
|
+
"id_number": "{{ p.account_holder_id | json_escape }}"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Atomic FI PaymentAccount → MDMInput JSON.
|
|
3
|
+
|
|
4
|
+
Used by Contract B's `mdm_input_config` (`resource_type:
|
|
5
|
+
"generic_table"`). A payment-account row references an account-holder
|
|
6
|
+
id (`p.account_holder_id`); resolving it routes through the LegalEntity
|
|
7
|
+
upsert, whose identification dedupe collapses repeat resolves sharing
|
|
8
|
+
the same `(id_type, id_number, uri)` triple onto the SAME LegalEntity
|
|
9
|
+
row Contract A wrote. The resolved subject's id is then stamped onto
|
|
10
|
+
the payment-account row as `legal_entity_id`.
|
|
11
|
+
|
|
12
|
+
GH-859 removed the `owner_type` / `account_holder_*` vocabulary along
|
|
13
|
+
with the AccountHolder resource itself: there is no LegalEntity +
|
|
14
|
+
AccountHolder pair to find-or-create, just the legal entity. The
|
|
15
|
+
identifier `uri` flows from `data_source.uri` — injected by the DAC as
|
|
16
|
+
`source_uri`, the same channel Contract A reads — so both contracts emit
|
|
17
|
+
an identical pair and converge on one subject.
|
|
18
|
+
|
|
19
|
+
NOTE the key names. An MDMInput identifier is `uri` / `type` / `value`. A
|
|
20
|
+
LegalEntity identification, which is what Contract A writes, is `id_type` /
|
|
21
|
+
`id_number` / `uri`. They are different shapes for the same idea, and
|
|
22
|
+
unknown keys are dropped rather than refused — so the wrong one resolves
|
|
23
|
+
nothing and fails as a bare `mdm_dispatcher_error` naming no field.
|
|
24
|
+
{% endcomment %}
|
|
25
|
+
{% assign p = msg %}
|
|
26
|
+
{% assign name_parts = p.account_holder_name | default: "" | split: " " %}
|
|
27
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
28
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
29
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
30
|
+
{
|
|
31
|
+
"legal_entity_type": "individual",
|
|
32
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
33
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.account_holder_id | json_escape }}",{% endif %}
|
|
34
|
+
"identifiers": [
|
|
35
|
+
{
|
|
36
|
+
"uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
|
|
37
|
+
"type": "digital_identifier",
|
|
38
|
+
"value": "{{ p.account_holder_id | json_escape }}"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
CAHPS Appointments → `appointments` generic_table row.
|
|
3
|
+
|
|
4
|
+
Contract B (`resource_type: "generic_table"`, `generic_table_id` pinned
|
|
5
|
+
to the appointments table). One JSON field per GT column.
|
|
6
|
+
|
|
7
|
+
GH-859 removed the `appointment` dataset along with every other
|
|
8
|
+
per-domain resource, so the FHIR R4 Appointment shape this template
|
|
9
|
+
used to emit — `participants[]` with `actor_type`, a nested identifier
|
|
10
|
+
array — has nowhere to go. An appointment is a table you declare, and
|
|
11
|
+
its columns are flat.
|
|
12
|
+
|
|
13
|
+
The load-bearing part survives unchanged: the vendor's
|
|
14
|
+
`appt_slot_status` string is mapped onto the canonical `status` the
|
|
15
|
+
workflow filter reads. That mapping is the contract's job, not the
|
|
16
|
+
workflow's — a filter that had to know about `"3 - checked out"` would
|
|
17
|
+
be coupled to one vendor's export format.
|
|
18
|
+
|
|
19
|
+
`legal_entity_id` is NEVER declared or written here. Contract B's
|
|
20
|
+
`mdm_input_config` resolves the patient and the platform stamps the
|
|
21
|
+
subject's id onto the row; the name is reserved, so declaring a column
|
|
22
|
+
of it 422s this create rather than shadowing the stamp.
|
|
23
|
+
|
|
24
|
+
GT columns:
|
|
25
|
+
* `appointment_id` — the vendor's id (unique, so a re-ingest updates)
|
|
26
|
+
* `status` — canonical: fulfilled / cancelled / arrived /
|
|
27
|
+
booked / pending
|
|
28
|
+
* `appt_start` — vendor `3/12/2026` + `2:30 PM` composed into one
|
|
29
|
+
ISO 8601 local timestamp
|
|
30
|
+
* `minutes_duration`— integer, so downstream analytics can average it
|
|
31
|
+
* `description`, `department`, `provider_id`, `provider_name`
|
|
32
|
+
* `patient_name`, `patient_mobile` — tokenized PII, and the SMS
|
|
33
|
+
destination the workflow's action templates read
|
|
34
|
+
* `patient_ref` — the vendor's patient id, carried so a row can be
|
|
35
|
+
traced back to the export without joining the subject
|
|
36
|
+
* `care_gaps` — jsonb ARRAY of open care gaps. Declared
|
|
37
|
+
`jsonb` + `is_array: true` + `redact`, which makes it the one
|
|
38
|
+
column here that exercises all three of those at once.
|
|
39
|
+
|
|
40
|
+
NOTE the explicit loop on `care_gaps`. Liquid's `to_json` on a nested
|
|
41
|
+
array renders Ruby-ish inspect output rather than JSON, so each entry is
|
|
42
|
+
written field by field. An absent `care_gaps` renders `[]` — a real
|
|
43
|
+
empty array, not null — which is what the column expects.
|
|
44
|
+
{% endcomment %}
|
|
45
|
+
{% assign a = msg %}
|
|
46
|
+
{% assign raw_status = a.appt_slot_status | default: "" | downcase | strip %}
|
|
47
|
+
{% assign status = "pending" %}
|
|
48
|
+
{% if raw_status == "2 - checked in" %}{% assign status = "arrived" %}
|
|
49
|
+
{% elsif raw_status == "3 - checked out" or raw_status == "4 - charge entered" %}{% assign status = "fulfilled" %}
|
|
50
|
+
{% elsif raw_status == "f - filled" %}{% assign status = "booked" %}
|
|
51
|
+
{% elsif raw_status == "x - cancelled" %}{% assign status = "cancelled" %}
|
|
52
|
+
{% endif %}
|
|
53
|
+
{
|
|
54
|
+
"appointment_id": "{{ a.appointment_id | default: "" | json_escape }}",
|
|
55
|
+
"status": "{{ status }}",
|
|
56
|
+
{% if a.appt_date and a.appt_date != "" %}"appt_start": "{{ a.appt_date | parse_date: "{M}/{D}/{YYYY}" }}T{% if a.appt_start_time and a.appt_start_time != "" %}{{ a.appt_start_time | convert_time: "{h12}:{m} {am}" }}{% else %}00:00:00{% endif %}",{% endif %}
|
|
57
|
+
{% if a.appt_slot_duration and a.appt_slot_duration != "" %}"minutes_duration": {{ a.appt_slot_duration }},{% endif %}
|
|
58
|
+
"description": "{{ a.appt_type | default: "" | json_escape }}",
|
|
59
|
+
"department": "{{ a.svc_department | default: "" | json_escape }}",
|
|
60
|
+
"provider_id": "{{ a.rndrng_provider_id | default: "" | json_escape }}",
|
|
61
|
+
"provider_name": "{{ a.rndrng_provider | default: "" | json_escape }}",
|
|
62
|
+
"patient_name": "{{ a.patient_name | default: "" | json_escape }}",
|
|
63
|
+
"patient_mobile": "{{ a.patient_mobile_no | default: "" | json_escape }}",
|
|
64
|
+
"patient_ref": "{{ a.patient_id | default: "" | json_escape }}",
|
|
65
|
+
"care_gaps": [{% for g in a.care_gaps %}{
|
|
66
|
+
"code": "{{ g.code | json_escape }}",
|
|
67
|
+
"description": "{{ g.description | default: "" | json_escape }}",
|
|
68
|
+
"due_date": "{{ g.due_date | default: "" | json_escape }}"
|
|
69
|
+
}{% unless forloop.last %},{% endunless %}{% endfor %}]
|
|
70
|
+
}
|
package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
CAHPS Appointments → LegalEntity attrs.
|
|
3
|
+
|
|
4
|
+
Contract A (`resource_type: "legal_entity"`, `mdm_input_config`
|
|
5
|
+
`{ type: "null" }`) — the patient on the CAHPS row IS the MDM subject,
|
|
6
|
+
so this template writes the LegalEntity directly.
|
|
7
|
+
|
|
8
|
+
GH-859 removed the per-domain identity schemas: there is no `patient`
|
|
9
|
+
resource and no FHIR R4 Patient shape to fill in. One LegalEntity
|
|
10
|
+
schema carries every subject the platform knows, and it is flat —
|
|
11
|
+
`first_name` / `last_name` / `date_of_birth` / `phone_numbers` /
|
|
12
|
+
`identifications`, not `name[].given` / `birth_date` / `telecom[]`.
|
|
13
|
+
|
|
14
|
+
## Identifications
|
|
15
|
+
* `patient-id` — always present, the practice's own patient id
|
|
16
|
+
* `enterprise-id` — only when the CAHPS export carries one (a
|
|
17
|
+
multi-practice enterprise MRN)
|
|
18
|
+
|
|
19
|
+
Both use `id_type: "digital_identifier"` with the verifying platform's
|
|
20
|
+
URI, which the DAC injects from the configured `data_source.uri` as
|
|
21
|
+
`source_uri`. Contract B's MDM input emits the SAME `(uri, patient_id)`
|
|
22
|
+
pair, which is what makes both contracts converge on one LegalEntity
|
|
23
|
+
per patient rather than two.
|
|
24
|
+
|
|
25
|
+
Parent-row filtering is handled by the contract's `filter_template`,
|
|
26
|
+
not here.
|
|
27
|
+
{% endcomment %}
|
|
28
|
+
{% assign p = msg %}
|
|
29
|
+
{% assign name_parts = p.patient_name | default: "" | split: " " %}
|
|
30
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
31
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
32
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
33
|
+
{
|
|
34
|
+
"legal_entity_type": "individual",
|
|
35
|
+
"role": "direct",
|
|
36
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
37
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.patient_name | default: "" | strip | json_escape }}",{% endif %}
|
|
38
|
+
{% if p.patientdob and p.patientdob != "" %}"date_of_birth": "{{ p.patientdob | parse_date: "{M}/{D}/{YYYY}" }}",{% endif %}
|
|
39
|
+
{% if p.patient_mobile_no and p.patient_mobile_no != "" %}"phone_numbers": [{ "phone_number": "{{ p.patient_mobile_no | json_escape }}" }],{% endif %}
|
|
40
|
+
"identifications": [
|
|
41
|
+
{
|
|
42
|
+
"id_type": "digital_identifier",
|
|
43
|
+
"uri": "{{ p.source_uri | json_escape }}",
|
|
44
|
+
"id_number": "{{ p.patient_id | default: "" | json_escape }}"
|
|
45
|
+
}{% if p.enterprise_id and p.enterprise_id != "" %},
|
|
46
|
+
{
|
|
47
|
+
"id_type": "digital_identifier",
|
|
48
|
+
"uri": "{{ p.source_uri | json_escape }}:enterprise",
|
|
49
|
+
"id_number": "{{ p.enterprise_id | json_escape }}"
|
|
50
|
+
}{% endif %}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
CAHPS Appointments → MDMInput JSON.
|
|
3
|
+
|
|
4
|
+
Used by Contract B's `mdm_input_config` (`resource_type:
|
|
5
|
+
"generic_table"`). The DAC pipeline renders this against the CAHPS row,
|
|
6
|
+
validates the result as an MDM input, and resolves it — routing the
|
|
7
|
+
attrs through the LegalEntity upsert, whose identification dedupe
|
|
8
|
+
collapses repeat resolves sharing the same `(id_type, id_number, uri)`
|
|
9
|
+
triple onto the SAME LegalEntity row Contract A wrote. The resolved
|
|
10
|
+
subject's id is then stamped onto the appointment row as
|
|
11
|
+
`legal_entity_id`.
|
|
12
|
+
|
|
13
|
+
The `system` value flows from `data_source.uri`, injected by the DAC as
|
|
14
|
+
`source_uri` — the same channel Contract A's template reads — so both
|
|
15
|
+
contracts emit an identical `(uri, patient_id)` pair and converge on
|
|
16
|
+
one LegalEntity per patient.
|
|
17
|
+
|
|
18
|
+
GH-859 removed the per-domain identity vocabularies. There is ONE field
|
|
19
|
+
set: `first_name` / `last_name` / `date_of_birth` / `phone` / `email` /
|
|
20
|
+
`identifiers`. `given_name`, `family_name` and `birth_date` are gone —
|
|
21
|
+
not renamed variants to pick between, and a template still sending them
|
|
22
|
+
resolves nothing rather than failing loudly.
|
|
23
|
+
{% endcomment %}
|
|
24
|
+
{% assign p = msg %}
|
|
25
|
+
{% assign name_parts = p.patient_name | default: "" | split: " " %}
|
|
26
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
27
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
28
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
29
|
+
{
|
|
30
|
+
"legal_entity_type": "individual",
|
|
31
|
+
{% if first_name != "" and last_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
32
|
+
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% else %}"last_name": "{{ p.patient_name | default: "" | strip | json_escape }}",{% endif %}
|
|
33
|
+
{% if p.patientdob and p.patientdob != "" %}"date_of_birth": "{{ p.patientdob | parse_date: "{M}/{D}/{YYYY}" }}",{% endif %}
|
|
34
|
+
{% if p.patient_mobile_no and p.patient_mobile_no != "" %}"phone": "{{ p.patient_mobile_no | json_escape }}",{% endif %}
|
|
35
|
+
"identifiers": [
|
|
36
|
+
{
|
|
37
|
+
"system": "{{ p.source_uri | json_escape }}",
|
|
38
|
+
"value": "{{ p.patient_id | default: "" | json_escape }}",
|
|
39
|
+
"type": "digital_identifier"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Stripe Customers → `customers` generic_table row.
|
|
3
|
+
|
|
4
|
+
Contract B (`resource_type: "generic_table"`, `generic_table_id`
|
|
5
|
+
pinned to the Stripe Customers table). One JSON field per GT column.
|
|
6
|
+
|
|
7
|
+
Contract B also carries an `mdm_input_config` (separate template), so
|
|
8
|
+
the platform resolves each row's subject and stamps `legal_entity_id`
|
|
9
|
+
onto the row. `legal_entity_id` is NEVER declared as a column and
|
|
10
|
+
never written here — the stamp is the platform's, and the name is
|
|
11
|
+
reserved, so declaring it 422s this create rather than shadowing it.
|
|
12
|
+
|
|
13
|
+
GT columns:
|
|
14
|
+
* `customer_number` — Stripe's customer id (the unique column, so a
|
|
15
|
+
re-fetch updates the same row instead of inserting a duplicate)
|
|
16
|
+
* `customer_type`, `status`, `currency` — commercial state, not PII
|
|
17
|
+
* `name`, `email`, `phone`, `tax_id` — tokenized PII. `tax_id` is
|
|
18
|
+
the KYC gate the dunning workflow filters on, so a template that
|
|
19
|
+
drops it makes every row look KYC-incomplete and the workflow
|
|
20
|
+
filters all of them — green ingest, silently empty outcome.
|
|
21
|
+
* `delinquent` — boolean; Stripe sends the string "false"/"true",
|
|
22
|
+
which renders unquoted here and lands as a real boolean
|
|
23
|
+
* `address_city`, `address_country` — coarse location, not PII
|
|
24
|
+
{% endcomment %}
|
|
25
|
+
{% assign p = msg %}
|
|
26
|
+
{
|
|
27
|
+
"customer_number": "{{ p.customer_number | json_escape }}",
|
|
28
|
+
"customer_type": "{{ p.customer_type | default: "individual" | json_escape }}",
|
|
29
|
+
"status": "{{ p.status | default: "prospect" | json_escape }}",
|
|
30
|
+
"name": "{{ p.name | default: "" | json_escape }}",
|
|
31
|
+
"email": "{{ p.email | default: "" | json_escape }}",
|
|
32
|
+
"phone": "{{ p.phone | default: "" | json_escape }}",
|
|
33
|
+
"tax_id": "{{ p.tax_id | default: "" | json_escape }}",
|
|
34
|
+
"currency": "{{ p.currency | default: "" | json_escape }}",
|
|
35
|
+
"delinquent": {{ p.delinquent | default: "false" }},
|
|
36
|
+
"address_city": "{{ p.address_city | default: "" | json_escape }}",
|
|
37
|
+
"address_country": "{{ p.address_country | default: "" | json_escape }}"
|
|
38
|
+
}
|
package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Stripe Customers → LegalEntity attrs.
|
|
3
|
+
|
|
4
|
+
Contract A (`resource_type: "legal_entity"`, `mdm_input_config`
|
|
5
|
+
`{ type: "null" }`) — this template writes the LegalEntity directly,
|
|
6
|
+
because the fetched Stripe customer IS the MDM subject.
|
|
7
|
+
|
|
8
|
+
## Branches
|
|
9
|
+
* `legal_entity_type: "individual"` when Stripe's `customer_type`
|
|
10
|
+
is `individual` — the display name splits into first/last
|
|
11
|
+
* `legal_entity_type: "business"` otherwise (`enterprise`, …) — the
|
|
12
|
+
display name becomes `business_name` and no first/last is written,
|
|
13
|
+
because a business does not carry them
|
|
14
|
+
|
|
15
|
+
## Identifications
|
|
16
|
+
* One per row: `id_type: "digital_identifier"`, `uri: <p.source_uri>`
|
|
17
|
+
(the DAC injects the configured `data_source.uri`),
|
|
18
|
+
`id_number: <customer_number>`. Contract B's MDM input emits the
|
|
19
|
+
SAME `(uri, value)` pair, which is what makes both contracts
|
|
20
|
+
converge on one LegalEntity row per customer instead of two.
|
|
21
|
+
|
|
22
|
+
There is ONE LegalEntity schema — GH-859 removed the public/regulated
|
|
23
|
+
pair, and with it the nested `regulated_customer` block this template
|
|
24
|
+
used to carry. Masking is infrastructure (the tokenized and redacted
|
|
25
|
+
lakes), not a second schema to write into.
|
|
26
|
+
{% endcomment %}
|
|
27
|
+
{% assign p = msg %}
|
|
28
|
+
{% assign le_type = "business" %}
|
|
29
|
+
{% if p.customer_type == "individual" %}{% assign le_type = "individual" %}{% endif %}
|
|
30
|
+
{% assign name_parts = p.name | default: "" | split: " " %}
|
|
31
|
+
{% assign first_name = name_parts[0] | default: "" %}
|
|
32
|
+
{% assign last_name_parts = name_parts | slice: 1, 9 %}
|
|
33
|
+
{% assign last_name = last_name_parts | join: " " %}
|
|
34
|
+
{
|
|
35
|
+
"legal_entity_type": "{{ le_type }}",
|
|
36
|
+
"role": "direct",
|
|
37
|
+
{% if le_type == "business" %}"business_name": "{{ p.name | json_escape }}",{% endif %}
|
|
38
|
+
{% if le_type == "individual" and first_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
39
|
+
{% if le_type == "individual" and last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% endif %}
|
|
40
|
+
{% if p.phone and p.phone != "" %}"phone_numbers": [{ "phone_number": "{{ p.phone | json_escape }}" }],{% endif %}
|
|
41
|
+
"identifications": [
|
|
42
|
+
{
|
|
43
|
+
"id_type": "digital_identifier",
|
|
44
|
+
"uri": "{{ p.source_uri | json_escape }}",
|
|
45
|
+
"id_number": "{{ p.customer_number | json_escape }}"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|