@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.agent/AGENTS.md +440 -0
  2. package/.agent/account_management.md +455 -0
  3. package/.agent/action_status_updaters.md +262 -0
  4. package/.agent/ai_agents.md +423 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +111 -0
  7. package/.agent/connected_apps.md +407 -0
  8. package/.agent/cookbook/_fixtures/README.md +99 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  14. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  17. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  18. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  19. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  21. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  22. package/.agent/cookbook/_setup/foundation.md +277 -0
  23. package/.agent/cookbook/_setup/healthcare.md +279 -0
  24. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  25. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  26. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  27. package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
  28. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  29. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  30. package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
  31. package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
  32. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  33. package/.agent/data_activation_clients.md +557 -0
  34. package/.agent/data_sources.md +234 -0
  35. package/.agent/datalakes.md +712 -0
  36. package/.agent/debugging.md +137 -0
  37. package/.agent/errors.md +196 -0
  38. package/.agent/generic_tables.md +351 -0
  39. package/.agent/interoperability_contracts.md +351 -0
  40. package/.agent/mdm.md +293 -0
  41. package/.agent/mutations.md +152 -0
  42. package/.agent/templates.md +98 -0
  43. package/.agent/tool-call-configs.md +90 -0
  44. package/.agent/tools.md +546 -0
  45. package/.agent/type_naming.md +131 -0
  46. package/.agent/workflows.md +601 -0
  47. package/README.md +46 -0
  48. package/dist/bin/platform-sdk.d.mts +1 -0
  49. package/dist/bin/platform-sdk.mjs +106 -0
  50. package/dist/bin/platform-sdk.mjs.map +1 -0
  51. package/dist/index.d.mts +1200 -43201
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +1859 -7319
  54. package/dist/index.mjs.map +1 -1
  55. package/package.json +19 -10
@@ -0,0 +1,88 @@
1
+ {% comment %}
2
+ Foundation Airflow Lead Form → Foundation LegalEntity attrs.
3
+
4
+ Maps a single Google-Sheet lead row into a Foundation
5
+ `LegalEntity` upsert payload. Used by Contract A
6
+ (`resource_type: "legal_entity"`, `mdm_input_type: :null`) — Contract A
7
+ writes the LE directly because the row IS the MDM subject for Foundation.
8
+ Identifier-based dedupe in `Foundation.LegalEntities.upsert_legal_entity`
9
+ collapses repeat submissions sharing the same digital_identifier
10
+ (verified email) onto a single LE row.
11
+
12
+ ## Branches
13
+ * `legal_entity_type: "individual"` when `company` is empty
14
+ * `legal_entity_type: "business"` when `company` is non-empty
15
+ (the company name becomes `business_name` and the submitter's name
16
+ is dropped — businesses don't carry first/last on the LE row)
17
+
18
+ ## Identifications
19
+ * One identification per row: `id_type: "digital_identifier"`,
20
+ `uri: <p.source_uri>` — the verifying platform's URI, which DAC
21
+ injects from the configured `data_source.uri` (e.g. the Airflow
22
+ form's host). `id_number: <email>`. Same triple is emitted on the
23
+ regulated nested attrs so the raw plaintext id_number lives on the
24
+ regulated mirror
25
+ (`Foundation.LegalEntities.find_legal_entity_by_identifications/2`
26
+ queries the regulated table — public id_number is tokenized).
27
+ {% endcomment %}
28
+ {% assign p = msg %}
29
+ {% assign le_type = "individual" %}
30
+ {% if p.company and p.company != "" %}{% assign le_type = "business" %}{% endif %}
31
+ {% assign name_parts = p.name | default: "" | split: " " %}
32
+ {% assign first_name = name_parts[0] | default: "" %}
33
+ {% assign last_name_parts = name_parts | slice: 1, 9 %}
34
+ {% assign last_name = last_name_parts | join: " " %}
35
+ {
36
+ "legal_entity_type": "{{ le_type }}",
37
+ "role": "direct",
38
+ {% if le_type == "business" %}"business_name": "{{ p.company | json_escape }}",{% endif %}
39
+ {% if le_type == "individual" and first_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
40
+ {% if le_type == "individual" and last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% endif %}
41
+ {% comment %}
42
+ Optional date_of_birth + phone_numbers — emitted at the TOP LEVEL
43
+ because `Platform.Foundation.LegalEntities.upsert_legal_entity/5`
44
+ feeds the SAME flat attrs map to both the public and the
45
+ regulated half's changeset (see the @doc above the function: "Both
46
+ halves consume the same flat `attrs` map — column names are
47
+ identical on the public and regulated schemas, so each side's
48
+ changeset reads the same keys"). Nesting these inside the
49
+ `regulated_legal_entity` block here would render them invisible
50
+ to the upsert (the regulated changeset reads `:date_of_birth` and
51
+ `:phone_numbers` from the flat top-level, not from a nested
52
+ object). The public LegalEntity schema does not declare
53
+ `:date_of_birth` or `has_many :phone_numbers`, so the public
54
+ changeset silently drops them; only the regulated mirror picks
55
+ them up.
56
+
57
+ `date_of_birth` is rendered as ISO 8601 YYYY-MM-DD; if the CSV
58
+ column is absent, the {% if %} guard renders nothing and the
59
+ regulated mirror's :date_of_birth stays nil.
60
+
61
+ `phone_numbers` renders as a single-element list — matches the
62
+ one-phone-per-lead shape of inbound lead-capture forms. Absent
63
+ column leaves the has_many empty.
64
+ {% endcomment %}
65
+ {% if p.date_of_birth and p.date_of_birth != "" %}"date_of_birth": "{{ p.date_of_birth | json_escape }}",{% endif %}
66
+ {% if p.phone and p.phone != "" %}"phone_numbers": [{ "phone_number": "{{ p.phone | json_escape }}" }],{% endif %}
67
+ "identifications": [
68
+ {
69
+ "id_type": "digital_identifier",
70
+ "uri": "{{ p.source_uri | json_escape }}",
71
+ "id_number": "{{ p.email | json_escape }}"
72
+ }
73
+ ],
74
+ "regulated_legal_entity": {
75
+ "legal_entity_type": "{{ le_type }}",
76
+ "role": "direct",
77
+ {% if le_type == "business" %}"business_name": "{{ p.company | json_escape }}",{% endif %}
78
+ {% if le_type == "individual" and first_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
79
+ {% if le_type == "individual" and last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% endif %}
80
+ "identifications": [
81
+ {
82
+ "id_type": "digital_identifier",
83
+ "uri": "{{ p.source_uri | json_escape }}",
84
+ "id_number": "{{ p.email | json_escape }}"
85
+ }
86
+ ]
87
+ }
88
+ }
@@ -0,0 +1,48 @@
1
+ {% comment %}
2
+ Foundation Airflow Lead Form → Foundation MDMInput JSON.
3
+
4
+ Used by Contract B's `mdm_input_config` (`resource_type: "generic_table"`).
5
+ The DAC pipeline renders this template against the lead row, validates
6
+ the result against `Platform.Foundation.MDMInput`, and feeds the
7
+ validated struct into `Foundation.MDMDispatcher.resolve/5` — which:
8
+
9
+ 1. Translates `MDMInput.identifiers` → Foundation identifications
10
+ (system URI → `id_type`, with unrecognised systems collapsing to
11
+ `:digital_identifier`).
12
+ 2. Routes the attrs through `LegalEntities.upsert_legal_entity/5`,
13
+ whose `find_legal_entity_by_identifications/2` dedupe path collapses
14
+ repeat resolves sharing the same `(id_type, id_number, uri)` triple
15
+ onto the SAME LegalEntity row written by Contract A.
16
+ 3. Pins the resulting GT event's `mdm_subject_id == legal_entity.id`.
17
+
18
+ The verifying platform URI flows from `data_source.uri` injected by DAC
19
+ as `source_uri` — same upstream channel as Contract A's LE template, so
20
+ both contracts produce identifications with identical `(uri, value)`
21
+ shape, guaranteeing dedupe convergence on a single LE.
22
+
23
+ ## Branches
24
+ * `legal_entity_type: "individual"` when `company` is empty
25
+ * `legal_entity_type: "business"` when `company` is non-empty (the
26
+ company name becomes `business_name`)
27
+ {% endcomment %}
28
+ {% assign p = msg %}
29
+ {% assign le_type = "individual" %}
30
+ {% if p.company and p.company != "" %}{% assign le_type = "business" %}{% endif %}
31
+ {% assign name_parts = p.name | default: "" | split: " " %}
32
+ {% assign first_name = name_parts[0] | default: "" %}
33
+ {% assign last_name_parts = name_parts | slice: 1, 9 %}
34
+ {% assign last_name = last_name_parts | join: " " %}
35
+ {
36
+ "legal_entity_type": "{{ le_type }}",
37
+ {% if le_type == "business" %}"business_name": "{{ p.company | 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.email and p.email != "" %}"email": "{{ p.email | json_escape }}",{% endif %}
41
+ {% if p.phone and p.phone != "" %}"phone": "{{ p.phone | json_escape }}",{% endif %}
42
+ "identifiers": [
43
+ {
44
+ "system": "{{ p.source_uri | json_escape }}",
45
+ "value": "{{ p.email | json_escape }}"
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,47 @@
1
+ {% comment %}CAHPS Appointments → FHIR R4 Appointment attrs{% endcomment %}
2
+ {% comment %}Parent appointment filtering is handled by filter_template{% endcomment %}
3
+ {% assign a = msg %}
4
+ {
5
+ {% if a.appt_slot_status %}
6
+ {% assign raw_status = a.appt_slot_status | downcase | strip %}
7
+ {% if raw_status == "2 - checked in" %}
8
+ "status": "arrived"
9
+ {% elsif raw_status == "3 - checked out" or raw_status == "4 - charge entered" %}
10
+ "status": "fulfilled"
11
+ {% elsif raw_status == "f - filled" %}
12
+ "status": "booked"
13
+ {% elsif raw_status == "o - open slot" %}
14
+ "status": "pending"
15
+ {% elsif raw_status == "x - cancelled" %}
16
+ "status": "cancelled"
17
+ {% endif %}
18
+ {% endif %},
19
+ {% if a.appt_date %}"start": "{{ a.appt_date | parse_date: "{M}/{D}/{YYYY}" }}{% if a.appt_start_time %}T{{ a.appt_start_time | convert_time: "{h12}:{m} {am}" }}{% endif %}{{ a.timezone | tz_offset }}",{% endif %}
20
+ {% if a.appt_slot_duration and a.appt_slot_duration != "" %}"minutes_duration": {{ a.appt_slot_duration }},{% endif %}
21
+ {% if a.appt_type and a.appt_type != "" %}"description": "{{ a.appt_type }}",{% endif %}
22
+ "identifier": [
23
+ {
24
+ "system": "urn:{{ a.source_uri }}:appointment-id",
25
+ "value": "{{ a.appointment_id | default: "" }}"
26
+ }
27
+ ],
28
+ "participants": [
29
+ {% if a.svc_department and a.svc_department != "" %}
30
+ {
31
+ "actor_type": "location",
32
+ "actor_id": "{{ a.svc_department }}",
33
+ "required": "required",
34
+ "status": "accepted"
35
+ }{% if a.rndrng_provider_id and a.rndrng_provider_id != "" %},{% endif %}
36
+ {% endif %}
37
+ {% if a.rndrng_provider_id and a.rndrng_provider_id != "" %}
38
+ {
39
+ "actor_type": "practitioner",
40
+ "actor_id": "{{ a.rndrng_provider_id }}",
41
+ "required": "required",
42
+ "status": "accepted"
43
+ }
44
+ {% endif %}
45
+ ],
46
+ "source_uri": "{{ a.source_uri | default: "" }}"
47
+ }
@@ -0,0 +1,24 @@
1
+ {% comment %}CAHPS Appointments → Healthcare MDM Input{% endcomment %}
2
+ {% assign p = msg %}
3
+ {
4
+ "identifiers": [
5
+ {
6
+ "system": "urn:{{ p.source_uri }}:patient-id",
7
+ "value": "{{ p.patient_id }}"
8
+ }{% if p.enterprise_id and p.enterprise_id != "" %},
9
+ {
10
+ "system": "urn:{{ p.source_uri }}:enterprise-id",
11
+ "value": "{{ p.enterprise_id }}"
12
+ }{% endif %}
13
+ ],
14
+ {% assign space_parts = p.patient_name | split: " " %}
15
+ {% if space_parts.size > 1 %}
16
+ "given_name": "{{ space_parts | first }}",
17
+ "family_name": "{{ space_parts | last }}",
18
+ {% else %}
19
+ "family_name": "{{ p.patient_name | strip }}",
20
+ {% endif %}
21
+ {% if p.patientdob and p.patientdob != "" %}"birth_date": "{{ p.patientdob | parse_date: "{M}/{D}/{YYYY}" }}",{% endif %}
22
+ {% if p.patient_mobile_no and p.patient_mobile_no != "" %}"phone": "{{ p.patient_mobile_no }}",{% endif %}
23
+ "gender": "unknown"
24
+ }
@@ -0,0 +1,38 @@
1
+ {% comment %}CAHPS Appointments → FHIR R4 Patient attrs{% endcomment %}
2
+ {% comment %}Parent appointment filtering is handled by filter_template{% endcomment %}
3
+ {% assign p = msg %}
4
+ {
5
+ "active": true,
6
+ "gender": "unknown",
7
+ {% if p.patientdob %}"birth_date": "{{ p.patientdob | parse_date: "{M}/{D}/{YYYY}" }}",{% endif %}
8
+ "identifier": [
9
+ {
10
+ "system": "urn:{{ p.source_uri }}:patient-id",
11
+ "value": "{{ p.patient_id | default: "" }}"
12
+ }{% if p.enterprise_id and p.enterprise_id != "" %},
13
+ {
14
+ "system": "urn:{{ p.source_uri }}:enterprise-id",
15
+ "value": "{{ p.enterprise_id }}"
16
+ }{% endif %}
17
+ ],
18
+ {% if p.patient_name %}"name": [
19
+ {
20
+ "use": "official",
21
+ {% assign space_parts = p.patient_name | split: " " %}
22
+ {% if space_parts.size > 1 %}
23
+ "given": ["{{ space_parts | first }}"],
24
+ "family": "{{ space_parts | last }}"
25
+ {% else %}
26
+ "family": "{{ p.patient_name | strip }}"
27
+ {% endif %}
28
+ }
29
+ ],{% endif %}
30
+ {% if p.patient_mobile_no and p.patient_mobile_no != "" %}"telecom": [
31
+ {
32
+ "system": "phone",
33
+ "value": "{{ p.patient_mobile_no }}",
34
+ "use": "mobile"
35
+ }
36
+ ],{% endif %}
37
+ "source_uri": "{{ p.source_uri | default: "" }}"
38
+ }
@@ -0,0 +1,59 @@
1
+ {% comment %}Atomic FI ComplianceScreenings → PaymentRisk ComplianceScreening attrs
2
+
3
+ Source: GET /api/compliance-screenings on Atomic FI (paginated; the
4
+ fetch pipeline extracts the `data` array via the response_extractor_key
5
+ and streams one row per `msg`).
6
+
7
+ The FK columns `legal_entity_id` and `account_holder_id` are NOT emitted
8
+ here — `ComplianceScreening.upsert_changeset/9` `put_change`s them from
9
+ the resolved MDMOutput after the MDM template's MDMInput drives
10
+ `MDM.resolve/5` (which find-or-creates the LE+AH parent pair).
11
+
12
+ Field handling on the unregulated side:
13
+ * narrative + name fields (screened_entity_name, review_notes,
14
+ pep_list_name) are persisted through TokenizedDataType — pass them
15
+ as raw strings here; the changeset's `cast_tokenized_data/4` calls
16
+ tokenize automatically before insert.
17
+ * analytic numerics (aml_risk_score, aml_velocity_count,
18
+ aml_high_risk_country) stay plaintext on both unreg + reg.
19
+ * `sanctions_matches` is explicitly looped — Liquid's default
20
+ rendering on a list produces a string, not JSON; the explicit
21
+ {% for %} loop emits one JSON object per element so the CS
22
+ upsert's `sync_sanctions_matches/6` can cast each child row.
23
+ {% endcomment %}
24
+ {% assign p = msg %}
25
+ {
26
+ "compliance_screening_number": "{{ p.compliance_screening_number | default: p.id | json_escape }}",
27
+ "scope": "{{ p.scope | default: "account_holder" | json_escape }}",
28
+ "screening_type": "{{ p.screening_type | default: "sanctions" | json_escape }}",
29
+ "screening_status": "{{ p.screening_status | default: "pending" | json_escape }}",
30
+ "screened_entity_type": "{{ p.screened_entity_type | default: "individual" | json_escape }}",
31
+ {% if p.screening_score and p.screening_score != "" %}"screening_score": {{ p.screening_score }},{% endif %}
32
+ {% if p.match_count and p.match_count != "" %}"match_count": {{ p.match_count }},{% endif %}
33
+ {% if p.screened_at and p.screened_at != "" %}"screened_at": "{{ p.screened_at | json_escape }}",{% endif %}
34
+ {% if p.sanctions_screening_status and p.sanctions_screening_status != "" %}"sanctions_screening_status": "{{ p.sanctions_screening_status | json_escape }}",{% endif %}
35
+ {% if p.sanctions_screening_date and p.sanctions_screening_date != "" %}"sanctions_screening_date": "{{ p.sanctions_screening_date | json_escape }}",{% endif %}
36
+ {% if p.manual_review_required %}"manual_review_required": {{ p.manual_review_required }},{% endif %}
37
+ {% if p.screened_entity_name and p.screened_entity_name != "" %}"screened_entity_name": "{{ p.screened_entity_name | json_escape }}",{% endif %}
38
+ {% if p.review_notes and p.review_notes != "" %}"review_notes": "{{ p.review_notes | json_escape }}",{% endif %}
39
+ {% if p.pep_list_name and p.pep_list_name != "" %}"pep_list_name": "{{ p.pep_list_name | json_escape }}",{% endif %}
40
+ {% if p.aml_risk_score and p.aml_risk_score != "" %}"aml_risk_score": {{ p.aml_risk_score }},{% endif %}
41
+ {% if p.aml_velocity_count and p.aml_velocity_count != "" %}"aml_velocity_count": {{ p.aml_velocity_count }},{% endif %}
42
+ {% if p.aml_high_risk_country and p.aml_high_risk_country != "" %}"aml_high_risk_country": "{{ p.aml_high_risk_country | json_escape }}",{% endif %}
43
+ {% if p.sanctions_matches %}
44
+ "sanctions_matches": [
45
+ {% for m in p.sanctions_matches %}
46
+ {
47
+ "matched_name": "{{ m.matched_name | json_escape }}",
48
+ {% if m.matched_entity_type and m.matched_entity_type != "" %}"matched_entity_type": "{{ m.matched_entity_type | json_escape }}",{% endif %}
49
+ "match_score": {{ m.match_score }},
50
+ "source_list": "{{ m.source_list | json_escape }}",
51
+ {% if m.source_id and m.source_id != "" %}"source_id": "{{ m.source_id | json_escape }}",{% endif %}
52
+ {% if m.false_positive_qualifier and m.false_positive_qualifier != "" %}"false_positive_qualifier": "{{ m.false_positive_qualifier | json_escape }}",{% endif %}
53
+ {% if m.list_synced_at and m.list_synced_at != "" %}"list_synced_at": "{{ m.list_synced_at | json_escape }}"{% else %}"list_synced_at": null{% endif %}
54
+ }{% unless forloop.last %},{% endunless %}
55
+ {% endfor %}
56
+ ],
57
+ {% endif %}
58
+ "source_uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}"
59
+ }
@@ -0,0 +1,36 @@
1
+ {% comment %}Atomic FI ComplianceScreenings → PaymentRisk MDM Input
2
+
3
+ A CS row from Atomic FI references an account_holder UUID
4
+ (`p.account_holder_id`). MDM-resolution side uses that as the external
5
+ `account_holder_number` to find the LegalEntity + AccountHolder pair
6
+ already ingested via the account-holders IOC.
7
+
8
+ `legal_entity_id` on the resulting MDMOutput is what links the CS row's
9
+ `legal_entity_id` (mandatory per the consistency trigger). The
10
+ `account_number_uri + account_holder_number` pair is the canonical
11
+ external identity used by `AccountHolders.match_account_holder/2`.
12
+ {% endcomment %}
13
+ {% assign p = msg %}
14
+ {
15
+ "owner_type": "account_holder",
16
+ "account_holder_type": "individual",
17
+ "account_holder_number": "{{ p.account_holder_id | json_escape }}",
18
+ "account_number_uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
19
+ "identifiers": [
20
+ {
21
+ "system": "urn:atomic.fi:account-holder-id",
22
+ "value": "{{ p.account_holder_id | json_escape }}",
23
+ "id_type": "digital_identifier"
24
+ }
25
+ ],
26
+ {% if p.screened_entity_name and p.screened_entity_name != "" %}
27
+ {% assign space_parts = p.screened_entity_name | split: " " %}
28
+ {% if space_parts.size > 1 %}
29
+ "given_name": "{{ space_parts | first | json_escape }}",
30
+ "family_name": "{{ space_parts | last | json_escape }}",
31
+ {% else %}
32
+ "family_name": "{{ p.screened_entity_name | strip | json_escape }}",
33
+ {% endif %}
34
+ {% endif %}
35
+ "gender": "unknown"
36
+ }
@@ -0,0 +1,30 @@
1
+ {% comment %}Atomic FI PaymentAccount → PaymentRisk MDM Input
2
+
3
+ A PaymentAccount row from Atomic FI references an account_holder UUID
4
+ (`p.account_holder_id`) — that's the canonical external identity used by
5
+ `AccountHolders.match_account_holder/2` to find-or-create the
6
+ LegalEntity + AccountHolder pair already ingested via the
7
+ account-holders IOC.
8
+
9
+ `legal_entity_id` on the resulting MDMOutput is what links the PA row's
10
+ `legal_entity_id` (PA belongs_to LE — provides the address slice for
11
+ sanctions-review override-agent context). The MDM scope is always
12
+ `account_holder` for PA rows because the PA inherently serves an
13
+ AccountHolder (per Atomic FI's PA schema invariant
14
+ `validate_required([:account_type, :account_holder_id, :currency,
15
+ :tenant_id])`).
16
+ {% endcomment %}
17
+ {% assign p = msg %}
18
+ {
19
+ "owner_type": "account_holder",
20
+ "account_holder_type": "individual",
21
+ "account_holder_number": "{{ p.account_holder_id | json_escape }}",
22
+ "account_number_uri": "{{ p.source_uri | default: "api.atomic.fi" | json_escape }}",
23
+ "identifiers": [
24
+ {
25
+ "system": "urn:atomic.fi:account-holder-id",
26
+ "value": "{{ p.account_holder_id | json_escape }}",
27
+ "id_type": "digital_identifier"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,55 @@
1
+ {% comment %}Atomic FI PaymentAccount → PaymentRisk PaymentAccount attrs
2
+
3
+ Source: GET /api/payment-accounts on Atomic FI (paginated; the fetch
4
+ pipeline extracts the `data` array via the response_extractor_key and
5
+ streams one row per `msg`).
6
+
7
+ FK columns (`legal_entity_id`, `account_holder_id`, `counterparty_id`)
8
+ are NOT emitted here — `PaymentAccount.upsert_changeset/7` `put_change`s
9
+ them from the resolved MDMOutput after the MDM template's MDMInput
10
+ drives `MDM.resolve/5`.
11
+
12
+ Field handling on the unregulated side:
13
+ * PCI-DSS-sensitive fields (`account_number`, `iban`, `card_pan`,
14
+ `wallet_address`) and narratives (`purpose_of_payment_detail`,
15
+ `business_relationship_description`) are persisted through
16
+ `TokenizedDataType` — pass them as raw strings here; the
17
+ changeset's `cast_tokenized_data/4` calls tokenize automatically
18
+ before insert.
19
+ * `enabled_regimes` is rendered as a JSON array; Liquid's `{{ x }}`
20
+ on a string array would produce a stringified Elixir list, not
21
+ JSON — the explicit `{% for %}` loop emits one quoted element per
22
+ regime so the changeset's array cast accepts it.
23
+
24
+ `destination_country` is derived here from `swift_bic` (chars 5-6 per
25
+ ISO 9362) or `iban` (chars 1-2 per ISO 13616), with BIC preferred when
26
+ both are present. Downstream agents (sanctions-review override) read
27
+ this field directly without re-parsing.
28
+ {% endcomment %}
29
+ {% assign p = msg %}
30
+ {% if p.swift_bic and p.swift_bic != "" %}
31
+ {% assign derived_country = p.swift_bic | slice: 4, 2 | upcase %}
32
+ {% elsif p.iban and p.iban != "" %}
33
+ {% assign derived_country = p.iban | slice: 0, 2 | upcase %}
34
+ {% else %}
35
+ {% assign derived_country = nil %}
36
+ {% endif %}
37
+ {
38
+ {% if p.payment_account_external_id and p.payment_account_external_id != "" %}"payment_account_external_id": "{{ p.payment_account_external_id | json_escape }}",{% endif %}
39
+ {% if p.payment_account_number and p.payment_account_number != "" %}"payment_account_number": "{{ p.payment_account_number | json_escape }}",{% endif %}
40
+ {% if p.status and p.status != "" %}"status": "{{ p.status | json_escape }}",{% endif %}
41
+ {% if p.account_type and p.account_type != "" %}"account_type": "{{ p.account_type | json_escape }}",{% endif %}
42
+ {% if p.currency and p.currency != "" %}"currency": "{{ p.currency | json_escape }}",{% endif %}
43
+ {% if p.routing_number and p.routing_number != "" %}"routing_number": "{{ p.routing_number | json_escape }}",{% endif %}
44
+ {% if p.swift_bic and p.swift_bic != "" %}"swift_bic": "{{ p.swift_bic | json_escape }}",{% endif %}
45
+ {% if p.bank_name and p.bank_name != "" %}"bank_name": "{{ p.bank_name | json_escape }}",{% endif %}
46
+ {% if p.wallet_chain and p.wallet_chain != "" %}"wallet_chain": "{{ p.wallet_chain | json_escape }}",{% endif %}
47
+ {% if p.account_number and p.account_number != "" %}"account_number": "{{ p.account_number | json_escape }}",{% endif %}
48
+ {% if p.iban and p.iban != "" %}"iban": "{{ p.iban | json_escape }}",{% endif %}
49
+ {% if p.card_pan and p.card_pan != "" %}"card_pan": "{{ p.card_pan | json_escape }}",{% endif %}
50
+ {% if p.wallet_address and p.wallet_address != "" %}"wallet_address": "{{ p.wallet_address | json_escape }}",{% endif %}
51
+ {% if p.purpose_of_payment_detail and p.purpose_of_payment_detail != "" %}"purpose_of_payment_detail": "{{ p.purpose_of_payment_detail | json_escape }}",{% endif %}
52
+ {% if p.business_relationship_description and p.business_relationship_description != "" %}"business_relationship_description": "{{ p.business_relationship_description | json_escape }}",{% endif %}
53
+ {% if derived_country and derived_country != "" %}"destination_country": "{{ derived_country | json_escape }}",{% endif %}
54
+ "enabled_regimes": [{% for r in p.enabled_regimes %}"{{ r | json_escape }}"{% unless forloop.last %},{% endunless %}{% endfor %}]
55
+ }