MOD-096 — Multi-entity Party Graph Manager (design)¶
System: SD02 (Customer Identity & KYC Platform) Repo: bank-kyc Phase: 2 of the Expense Intelligence Platform
1. What it does¶
MOD-096 maintains the directed graph of party entities for a single
customer login: the individual, any sole-trader personas they operate,
companies they direct, trusts they trustee, and property-context envelopes
they own. Nodes are typed (NATURAL_PERSON / ORGANISATION / ARRANGEMENT
top-level via the ADR-064 party.parties taxonomy; INDIVIDUAL /
SOLE_TRADER / LIMITED_COMPANY / TRUST / PROPERTY_CONTEXT operational
subtype via the V002 column landed in commit 8bba2d1). Edges are typed
relationships (IS_DIRECTOR_OF / IS_TRUSTEE_OF / IS_BENEFICIAL_OWNER_OF /
OPERATES_AS).
Adding a node:
1. POST /v1/graph/nodes — validates the request, creates the party row,
creates the edge, and either (a) PROPERTY_CONTEXT: lands the edge
ACTIVE immediately and emits bank.kyc.party_graph_edge_activated;
or (b) anything else: lands the edge PENDING_CDD and emits only
bank.kyc.party_graph_node_added — activation comes asynchronously.
Activation:
- MOD-009 publishes bank.kyc.identity_verified when eIDV completes
for a natural-person target. MOD-096's EventBridge consumer rule
activates the edge after enforcing the AML-002 GATE (a PASS row
must exist in kyc.kyc_checks).
- MOD-010 publishes bank.kyc.cdd_tier_assigned when CDD tier is set
for a business-entity target. MOD-096's other consumer rule
activates that edge. The cdd_tier is itself evidence that EDD has
run upstream — AML-004 GATE is satisfied implicitly by the
presence of a PASS tier (SIMPLIFIED / STANDARD / ENHANCED).
Reading the graph: - GET /v1/graph/{root_party_id} returns the full set of nodes + edges reachable from the login-rooted natural person. EXITED edges are filtered out. Used by SD08 (bank-app, MOD-094) to render the customer context switcher (FR-798).
2. FRs covered¶
| FR | Implementation |
|---|---|
| FR-796 | validateEdge() enforces five subtypes × four edge types; DDL |
| CHECKs back-stop. Negative integration cases in fr-796. | |
| FR-797 | add-node.ts parks edges PENDING_CDD; activate-edge.ts flips |
| them ACTIVE only when the inbound event carries / can resolve | |
| a PASS kyc_check_id (AML-002 GATE). | |
| FR-798 | list-graph.ts traverses bidirectional edges from a NATURAL_PERSON |
| root. |
3. Policy satisfaction¶
| Policy | Mode | Where |
|---|---|---|
| AML-002 | GATE | activateEdgesForParty() throws COMPLIANCE_BLOCK if no PASS |
| kyc_check_id is available on the identity_verified path. | ||
| Negative test: tests/integration/policy/aml-002-gate.test.ts | ||
| AML-004 | GATE | Edge activation refuses to flip on status=FAILED. EDD |
| tier (ENHANCED) is treated as evidence that EDD ran upstream | ||
| in MOD-010 (per the EDD workflow contract). | ||
| Negative test: tests/integration/policy/aml-004-gate.test.ts |
4. Tables¶
| Schema.table | DDL owner | MOD-096 access |
|---|---|---|
| party.parties | MOD-009 V001/V002 | INSERT, SELECT |
| party.party_relationships | MOD-009 V003 | INSERT, UPDATE, SELECT |
| kyc.kyc_checks | MOD-009 V001 | SELECT (AML-002 evidence) |
| kyc.idempotency_records | MOD-009 V002 | INSERT, SELECT (shared idempotency, keyPrefix=MOD-096) |
ADR-064 routing: all party.* DDL is centralised in MOD-009 (party_migrate_user owns the schema; kyc_migrate_user has no CREATE on party). MOD-096 ships no migrations of its own.
5. Events¶
Published (both registered into MOD-043's per-env Schema Registry):
- bank.kyc.party_graph_node_added v1
- bank.kyc.party_graph_edge_activated v1
Consumed (via EventBridge rules on the bank-kyc bus):
- bank.kyc.identity_verified (from MOD-009) → activate natural-person edges
- bank.kyc.cdd_tier_assigned (from MOD-010) → activate business-entity edges
6. SSM outputs¶
| Path | Consumer |
|---|---|
/bank/{env}/kyc/party-graph/api-endpoint |
MOD-094 (SD08) |
/bank/{env}/kyc/party-graph/function-arn |
infra tests, MOD-082 dashboards |
/bank/{env}/kyc/party-graph/function-name |
CloudWatch ops |
/bank/{env}/kyc/events/party-graph-node-added/schema-arn |
downstream consumers |
/bank/{env}/kyc/events/party-graph-edge-activated/schema-arn |
downstream consumers |
/bank/{env}/kyc/tables/party-relationships/name |
MOD-072 risk views |
Smoke check: node tests/verify-deployment.mjs (STAGE env var).
7. Upstream SSM inputs¶
| Path | Owner | Required at deploy |
|---|---|---|
/bank/{env}/iam/lambda/bank-kyc/arn |
MOD-104 | yes |
/bank/{env}/eventbridge/bank-kyc/arn |
MOD-104 | yes |
/bank/{env}/kms/pii/arn |
MOD-104 | yes |
/bank/{env}/mod043/schema-registry/name |
MOD-043 | yes |
/bank/{env}/observability/adot-layer-arn |
MOD-076 | yes |
/bank/{env}/observability/parameters-extension-layer-arn |
MOD-076 | yes |
/bank/{env}/kyc/eidv/api-endpoint |
MOD-009 | yes |
/bank/{env}/kyc/cdd/function-arn |
MOD-010 | optional |
Secret bank-neon/{env}/kyc_app_user |
MOD-103 | yes (Pulumi reads pooled_url at deploy) |
8. Orchestrator rulings (build-scope confirmation, 2026-05-18)¶
party.party_relationships— normalised table with(from_party_id, to_party_id, relationship_type)tuple, UNIQUE onidempotency_key, immutability trigger refusing DELETE + mutation of identity columns, governed lifecycle (no ACTIVE→PENDING_CDD regression, no leaving EXITED),is_uboauto-flip trigger at ≥25%, CHECK constraint requiringownership_percentageon IS_BENEFICIAL_OWNER_OF.- No PENDING_CDD timeout — edges sit indefinitely; an EMF CloudWatch
alarm (
mod-096-{stage}-pending-cdd-stale) fires at the configurablePENDING_CDD_ALERT_HOURSthreshold (default 336h = 14 days). Operator surfaces the stale edge via dashboards; no automatic state change. - PROPERTY_CONTEXT bypasses CDD — MOD-094 calls
POST /v1/graph/nodeswithparty_subtype=PROPERTY_CONTEXT; the handler skips the CDD trigger and lands the edge ACTIVE immediately (NO_CDD_SUBTYPES.has(party_subtype)). - Shared idempotency —
@bank-kyc/sharedPostgresIdempotencyStoreagainstkyc.idempotency_recordswithkeyPrefix="MOD-096", plus DB-level UNIQUE onparty_relationships.idempotency_keyas belt-and-braces (replay across cold containers falls through to the UNIQUE-violation path, which surfaces as 409 CONFLICT at the API). - IS_BENEFICIAL_OWNER_OF — direct-only for v1 —
ownership_percentageis required (CHECK + handler validation);is_uboflips TRUE at ≥25%. Chain traversal (UBOs of UBOs) is a read-time concern handled by MOD-072.
9. Test plan¶
Unit (≥80% coverage): tests/unit/
- graph/{validate-edges, add-node, activate-edge, list-graph}
- handler, events, db, idempotency, errors, observability, config
- lib/{schema-validator, eventbridge-client}
Integration (RUN_INTEGRATION=1, against deployed dev): - fr/fr-796-graph-structure - fr/fr-797-cdd-gate - fr/fr-798-context-switcher - policy/aml-002-gate (NEGATIVE) - policy/aml-004-gate (NEGATIVE) - infra/ssm-outputs - infra/iam-binds-bankkycrole - infra/eventbridge-rules - infra/eventbridge-schema-registry - infra/party-relationships-immutability - nfr/idempotency
Smoke: tests/verify-deployment.mjs.