ores.refdata.party
Table of Contents
- 1. Flags
- 2. Columns
- 3. Foreign keys
- 4. Insert trigger
- 5. SQL
- 6. Messages
- 7. C++
- 8. See also
Internal legal entities (the organisation and its subsidiaries) that
participate in financial transactions. Parties form a hierarchy through
parent_party_id, with exactly one root party per tenant representing the
organisation itself.
1. Flags
Cross-cutting properties. Consumed by both C++ and SQL codegen.
2. Columns
Logical, human-meaningful identifiers (full_name, short_code,
flagged :natural_key: true below) are inserted into the C++ struct
alongside the primary key, and exposed in SQL as additional
unique-indexed columns.
2.1. id
UUID uniquely identifying this party.
Surrogate key for the party record.
2.2. full_name
Full legal name of the party.
The official registered name of the entity.
std::string(faker::company::companyName())
2.3. short_code
Short code for quick reference.
A brief mnemonic code used in trading systems.
std::string(faker::string::alpha(6))
2.4. codename
Globally unique human-readable codename (adjective_noun).
Used as the per-party pgmq queue prefix, pg_cron job names, and operator tooling. Immutable once assigned; auto-generated by the SQL trigger if not supplied on insert.
""
2.5. transliterated_name
ASCII transliteration of the entity name.
Populated from GLEIF data for entities with non-Latin names (CJK, Cyrillic, Arabic, etc.). Null for entities already in Latin script.
std::nullopt
2.6. party_category
Structural classification of this party.
References the party_category lookup table. Values: 'System' (one per tenant, auto-created during provisioning) or 'Operational' (business entities created during normal system operation). Not user-editable via the UI – every party created through a detail form is 'Operational'; 'System' parties are exclusively auto-created during tenant provisioning. Defaults here so a freshly-constructed party (the Add form, with no UI field for this column) always carries a value the party_category-validation trigger accepts.
std::string("Operational")
2.7. party_type
Classification of this party.
References the party_type lookup table.
std::string("Corporate")
2.8. parent_party_id
Parent party for hierarchy.
References the parent party record for group structures. Null for root parties.
std::nullopt
2.9. business_center_code
Business center location code.
FpML business center code indicating primary location. Synthetic
parties default to WRLD (the global sentinel business centre,
guaranteed seeded for every tenant by the system-tenant bootstrap –
see the same reasoning in ores.refdata.book.org), not a real FpML
city code like GBLO: real city codes only exist once the large,
optional FpML business-centre reference dataset has been loaded, so
using one here would make a freshly-generated party fail its
FK-validation trigger in any environment that hasn't loaded it.
std::string("WRLD")
2.10. status
Current lifecycle status.
References the party_status lookup table.
std::string("Active")
2.11. image_id
Optional reference to a logo/branding image in the images table.
std::nullopt
3. Foreign keys
3.1. parent_party_id
3.2. image_id
4. Insert trigger
4.1. Validations
| column | validation_function |
|---|---|
| party_category | ores_refdata_validate_party_category_fn |
| party_type | ores_refdata_validate_party_type_fn |
| status | ores_refdata_validate_party_status_fn |
| business_center_code | ores_refdata_validate_business_centre_fn |
5. SQL
Everything below is consumed by the SQL codegen pipeline only.
5.1. Flags
5.2. Checks
| expression |
|---|
| length("codename") <= 32 |
| "codename" = '' or "codename" ~ '^[a-z][a-z_]+$' |
5.3. Indexes
| name | columns | unique | current_only | where_extra |
|---|---|---|---|---|
| full_name | tenant_id, full_name | false | true | |
| codename_uniq | codename | true | true | codename <> '' |
| root_party_uniq | tenant_id | true | false | parent_party_id is null and party_category <> 'System' and valid_to = ores_utility_infinity_timestamp_fn() |
| system_party_uniq | tenant_id | true | false | party_category = 'System' and valid_to = ores_utility_infinity_timestamp_fn() |
5.4. Codename sequence
Sequence used to generate a unique base-26 suffix for auto-generated
codenames. nextval() is outside MVCC: it advances even within a
multi-row INSERT statement, so every row in a batch gets a different
suffix, avoiding duplicate-key collisions that a NOT EXISTS loop
cannot prevent due to the statement-level snapshot in READ COMMITTED.
-- Sequence used to generate a unique base-26 suffix for auto-generated -- codenames. nextval() is outside MVCC: it advances even within a -- multi-row INSERT statement, so every row in a batch gets a different -- suffix, avoiding duplicate-key collisions that a NOT EXISTS loop cannot -- prevent due to the statement-level snapshot in READ COMMITTED. create sequence if not exists ores_refdata_party_codename_seq;
drop sequence if exists ores_refdata_party_codename_seq;
5.5. Codename immutability (update path)
Codename is immutable: restore it from the active row on every update, rather than letting a client-supplied value silently overwrite it.
-- Codename is immutable: restore from active row on every update. select codename into NEW.codename from "ores_refdata_parties_tbl" where tenant_id = NEW.tenant_id and id = NEW.id and valid_to = ores_utility_infinity_timestamp_fn();
5.6. Codename generation and queue provisioning (insert path)
Auto-generates codename (whimsical name + a globally-unique base-26
sequence suffix) when the client leaves it blank, validates the final
format, and provisions the per-party report-event queue – all only on
first insert, since codename is immutable thereafter (see above).
-- Auto-generate codename using whimsical name + sequence suffix. -- The sequence suffix is outside MVCC (nextval always advances), -- so every row in a multi-row INSERT gets a distinct suffix, making -- the codename globally unique without relying on a NOT EXISTS -- check that cannot see sibling rows in the same statement. if NEW.codename = '' or NEW.codename is null then NEW.codename := ores_utility_generate_whimsical_name_fn() || '_' || ores_utility_to_base26_fn(nextval('ores_refdata_party_codename_seq')); end if; -- Validate the final codename. if NEW.codename !~ '^[a-z][a-z_]+$' then raise exception 'codename must match ^[a-z][a-z_]+$ got: %', NEW.codename using errcode = '23514'; end if; -- Provision the per-party report event queue. perform ores_mq_queues_create_fn( NEW.tenant_id, NEW.id, 'party', 'task', 'report_events', 'Per-party report scheduling queue', current_user);
6. Messages
The composite_as_of pair, declared here so both the header and its
TypeScript twin render it.
6.1. get_party_composite_as_of_request
/** * @brief Reads a party as it stood at a specific version, together with its * identifiers and contact information as they stood during that same * version's [valid_from, valid_to) window. See the "Temporal composite * entity versioning" architecture doc. */
6.1.1. id
6.1.2. version
6.2. get_party_composite_as_of_response
6.2.1. success
6.2.2. message
6.2.3. party
6.2.4. identifiers
6.2.5. contacts
7. C++
Everything below is consumed by the C++ codegen pipeline only.
7.1. Flags
7.2. Repository
Reads are left to row level security rather than filtered again in the
query. IAM resolves a party's name while authenticating – the chooser a
caller picks from – and it authenticates as its own service account, which
lives in the system tenant. A party row belongs to the tenant that created
it, so an app-level filter of tenant_id = the caller admits none of them,
and every name in the chooser comes back empty. The policy on this table
admits the owning tenant or the system tenant, and that is the statement
that should decide it. Every other tenant still sees only its own rows.
7.3. Domain includes
#include <chrono> #include <string> #include <optional> #include <boost/uuid/uuid.hpp>
7.4. Entity includes
#include <string> #include <optional> #include "sqlgen/Timestamp.hpp" #include "sqlgen/PrimaryKey.hpp"
7.5. Conventions
7.6. Table display
| column | header |
|---|---|
| short_code | Code |
| full_name | Name |
| party_type | Type |
| status | Status |
| business_center_code | Business Center |
| modified_by | Modified By |
| version | Version |
7.7. Presentation
7.7.1. Detail fields
| field | label | widget | type | is_key | is_required | placeholder | flag_source | combo_allow_blank | combo_fetch | combo_domain_type | combo_display_field | combo_fetch_include | combo_fetch_fn | combo_watcher_name | combo_code_field | combo_tooltip_field | combo_sort_field | combo_label | combo_setter_pascal | badge_key | combo_blank_label |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| short_code | Short Code | codeEdit | line_edit | true | true | Enter short code | |||||||||||||||
| full_name | Full Name | nameEdit | line_edit | false | true | Enter full name | |||||||||||||||
| party_type | Party Type | partyTypeCombo | dynamic_combo | false | refdata::domain::party_type | fetch_party_types | partyTypeWatcher | code | description | display_order | party types | PartyTypeCombo | party_type | ||||||||
| status | Status | statusCombo | dynamic_combo | false | refdata::domain::party_status | fetch_party_statuses | partyStatusWatcher | code | description | display_order | party statuses | PartyStatusCombo | party_status | ||||||||
| business_center_code | Business Center | businessCenterCombo | flagged_combo | false | business_centre | false | fetch_business_centre_codes | ||||||||||||||
| parent_party_id | Parent Party | parentPartyCombo | dynamic_combo | false | true | refdata::domain::party | full_name | fetch_parties | parentPartyWatcher | id | short_code | version | parties | ParentPartyCombo | No Parent |
7.7.2. Columns
| enum_name | field | header | type | width | is_badge | badge_key | formatter | formatter_include |
|---|---|---|---|---|---|---|---|---|
| ShortCode | short_code | Code | string | 120 | ||||
| FullName | full_name | Name | string | 250 | ||||
| PartyType | party_type | Type | string | 120 | true | party_type | ||
| Status | status | Status | string | 100 | true | party_status | ||
| BusinessCenterCode | business_center_code | Business Center | string | 130 | ||||
| Version | version | Version | int | 80 | ||||
| ModifiedBy | modified_by | Modified By | string | 120 | ||||
| RecordedAt | recorded_at | Recorded At | timestamp | 150 |
7.7.3. Icon columns
Business center gets its own flag icon, same as any other single-business-centre cell (e.g. Book's rates centre code column).
| column | accessor | field1 | field2 |
|---|---|---|---|
| BusinessCenterCode | business_centre_flag_icon | business_center_code |
7.8. Custom repository methods
The two methods below are beyond the standard repository template because
they need raw SQL access: read_system_party (calls a stored procedure)
and read_descendants (a recursive CTE over the hierarchy). The
paginated read_latest(offset, limit) and get_total_party_count() used
to be hand-maintained here too, but are now covered by the generic
:service_pagination: true facet — removed to avoid duplicate
declarations. The codegen mechanism injects the remaining two at
template-rendered markers.
7.8.1. read_system_party
Calls ores_refdata_read_system_party_fn stored procedure. Every tenant has
exactly one system party (party_category'System'=) which serves as the
root of the party hierarchy.
std::vector<domain::party> read_system_party(context ctx, const std::string& tenant_id);
#include <boost/uuid/uuid_io.hpp> #include <boost/lexical_cast.hpp> #include "ores.utility/uuid/tenant_id.hpp"
std::vector<domain::party> party_repository::read_system_party(context ctx, const std::string& tenant_id) { BOOST_LOG_SEV(lg(), debug) << "Reading system party for tenant: " << tenant_id; const std::string sql = "SELECT * FROM ores_refdata_read_system_party_fn('" + tenant_id + "'::uuid)"; const auto rows = execute_raw_multi_column_query(ctx, sql, lg(), "Reading system party by tenant"); std::vector<domain::party> result; result.reserve(rows.size()); static constexpr std::array required_columns = {0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14}; for (const auto& row : rows) { if (row.size() >= 16 && std::ranges::all_of(required_columns, [&row](int i) { return static_cast<bool>(row[i]); })) { domain::party p; p.id = boost::lexical_cast<boost::uuids::uuid>(*row[0]); p.tenant_id = utility::uuid::tenant_id::from_string(*row[1]).value(); p.version = std::stoi(*row[2]); p.full_name = *row[3]; p.short_code = *row[4]; p.party_category = *row[5]; p.party_type = *row[6]; if (row[7]) p.parent_party_id = boost::lexical_cast<boost::uuids::uuid>(*row[7]); if (row[8]) p.business_center_code = *row[8]; p.status = *row[9]; p.modified_by = *row[10]; result.push_back(p); } } return result; }
7.8.2. read_descendants
Traverses the party hierarchy via a recursive CTE starting from root_id.
Returns the root plus every descendant (active records only).
std::vector<boost::uuids::uuid> read_descendants(context ctx, const boost::uuids::uuid& root_id);
#include <boost/uuid/uuid_io.hpp> #include <boost/lexical_cast.hpp>
std::vector<boost::uuids::uuid> party_repository::read_descendants(context ctx, const boost::uuids::uuid& root_id) { BOOST_LOG_SEV(lg(), debug) << "Reading party descendants. Root: " << root_id; const auto id_str = boost::uuids::to_string(root_id); const std::string sql = "WITH RECURSIVE party_tree AS (" " SELECT id FROM ores_refdata_parties_tbl" " WHERE id = '" + id_str + "' AND valid_to = '" + MAX_TIMESTAMP + "'" " UNION ALL" " SELECT p.id FROM ores_refdata_parties_tbl p" " JOIN party_tree pt ON p.parent_party_id = pt.id" " WHERE p.valid_to = '" + MAX_TIMESTAMP + "'" ") SELECT id FROM party_tree"; const auto rows = execute_raw_multi_column_query(ctx, sql, lg(), "Reading party descendants"); std::vector<boost::uuids::uuid> result; result.reserve(rows.size()); for (const auto& row : rows) if (!row.empty() && row[0]) result.push_back(boost::lexical_cast<boost::uuids::uuid>(*row[0])); return result; }
7.9. Custom NATS protocol messages
party declares one message pair beyond the standard CRUD surface and
generic facets, in the top-level * Messages section: composite_as_of
(reads a party as it stood at a specific version, together with its
identifiers and contact information as they stood during that same
version's window — see the "Temporal composite entity versioning"
architecture doc). The header and its TypeScript twin both render it from
there.
#include "ores.refdata.api/domain/party_contact_information.hpp" #include "ores.refdata.api/domain/party_identifier.hpp"
7.10. Custom NATS handler methods
Hand-written handler method for composite_as_of, dispatching to
party_service=/=party_identifier_service=/=party_contact_information_service.
#include "ores.refdata.core/service/party_contact_information_service.hpp" #include "ores.refdata.core/service/party_identifier_service.hpp" #include "ores.database/service/tenant_context.hpp" #include "ores.service/messaging/workflow_helpers.hpp" #include <chrono>
save's workflow-step-command branch (below) special-cases orchestration
commands ahead of the generated standard flow: it bypasses JWT auth and
uses the X-Tenant-Id header instead, replays a cached result if the step
already completed (idempotency), and reports completion/failure back to
the workflow engine rather than replying to the caller directly.
using ores::service::messaging::is_workflow_command; using ores::service::messaging::extract_workflow_header; using ores::service::messaging::publish_step_completion; using ores::service::messaging::check_step_idempotency; using ores::workflow::messaging::step_id_header; using ores::workflow::messaging::instance_id_header; using ores::workflow::messaging::tenant_id_header; // Workflow step command: bypass JWT auth; use X-Tenant-Id for context. if (is_workflow_command(msg)) { const auto step_id = extract_workflow_header(msg, step_id_header); const auto inst_id = extract_workflow_header(msg, instance_id_header); const auto tenant_id = extract_workflow_header(msg, tenant_id_header); // Idempotency guard: replay cached result if this step already completed. if (auto cached = check_step_idempotency(nats_, step_id)) { publish_step_completion(nats_, step_id, inst_id, cached->outcome, cached->result_json, cached->error_message, cached->log); return; } auto req = decode<put_party_request>(msg); if (!req) { publish_step_completion(nats_, step_id, inst_id, ores::workflow::messaging::step_outcome::failed, "", "Failed to decode put_party_request"); return; } try { using ores::database::service::tenant_context; auto wf_ctx = tenant_context::with_tenant(ctx_, tenant_id); service::party_service svc(wf_ctx); // The service answers the canonical response, whose result // states the outcome. A refusal is the step failing. const auto resp = svc.put_party(*req); if (resp.result.outcome != ores::utility::domain::outcome::ok) { publish_step_completion(nats_, step_id, inst_id, ores::workflow::messaging::step_outcome::failed, "", resp.result.message); return; } BOOST_LOG_SEV(party_handler_lg(), debug) << "Workflow step completed: " << msg.subject; publish_step_completion(nats_, step_id, inst_id, ores::workflow::messaging::step_outcome::completed, rfl::json::write(resp), ""); } catch (const std::exception& e) { BOOST_LOG_SEV(party_handler_lg(), error) << "Workflow step failed: " << msg.subject << " — " << e.what(); publish_step_completion(nats_, step_id, inst_id, ores::workflow::messaging::step_outcome::failed, "", e.what()); } return; }
void composite_as_of(ores::nats::message msg) { BOOST_LOG_SEV(party_handler_lg(), debug) << "Handling " << msg.subject; auto ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_); if (!ctx_expected) { error_reply(nats_, msg, ctx_expected.error()); return; } const auto& ctx = *ctx_expected; auto req = decode<get_party_composite_as_of_request>(msg); if (!req) { BOOST_LOG_SEV(party_handler_lg(), warn) << "Failed to decode: " << msg.subject; reply(nats_, msg, get_party_composite_as_of_response{.success = false, .message = "Failed to decode request"}); return; } try { service::party_service party_svc(ctx); const auto version = static_cast<std::uint32_t>(req->version); auto current = party_svc.get_party_at_version(req->id, version); if (!current) { reply(nats_, msg, get_party_composite_as_of_response{.success = false, .message = "No such party version: " + req->id + " v" + std::to_string(version)}); return; } // Windows are contiguous by construction: the next version's // valid_from is this version's valid_to. If there is no next // version, this is the current one — its window is still open, // so bound it with a safely-far-future instant instead (see the // "Temporal composite entity versioning" architecture doc; the // domain object does not surface valid_to directly). auto next = party_svc.get_party_at_version(req->id, version + 1); const auto window_end = next ? next->recorded_at : std::chrono::system_clock::now() + std::chrono::hours(24 * 365 * 100); service::party_identifier_service identifier_svc(ctx); auto identifiers = identifier_svc.list_party_identifiers_by_party_id_as_of( req->id, current->recorded_at, window_end); service::party_contact_information_service contact_svc(ctx); auto contacts = contact_svc.list_party_contact_informations_by_party_id_as_of( req->id, current->recorded_at, window_end); BOOST_LOG_SEV(party_handler_lg(), debug) << "Completed " << msg.subject; reply(nats_, msg, get_party_composite_as_of_response{.success = true, .party = std::move(*current), .identifiers = std::move(identifiers), .contacts = std::move(contacts)}); } catch (const std::exception& e) { BOOST_LOG_SEV(party_handler_lg(), error) << msg.subject << " failed: " << e.what(); reply(nats_, msg, get_party_composite_as_of_response{.success = false, .message = e.what()}); } }
7.11. Custom NATS subscriptions
Hand-crafted subjects wired into the generated party_registrar.cpp via the
nats-sub-registrar facet's custom-subscription paste point.
subs.push_back(nats.queue_subscribe(
get_party_composite_as_of_request::nats_subject, queue_group, [h](ores::nats::message msg) {
h->composite_as_of(std::move(msg));
}));
7.12. Cache aux index
The generated party_cache (nats-event-cache facet, cached_by:
iam) carries a parent/child aux index alongside the plain
tenant/id → party map, so IAM can answer "which parties are visible
from this root" without a DB round-trip. See the cache_aux_type flag
under * C++ / ** Flags and the facet doc's "Optional aux index"
section.
using children_map = immer::map<boost::uuids::uuid, std::vector<boost::uuids::uuid>, key_hash>;
auto children_t = children_map{}.transient(); for (const auto& [id, p] : entries) { if (p.parent_party_id) { const auto* existing = children_t.find(*p.parent_party_id); auto siblings = existing ? *existing : std::vector<boost::uuids::uuid>{}; siblings.push_back(id); children_t.set(*p.parent_party_id, std::move(siblings)); } } auto aux = children_t.persistent();
std::vector<boost::uuids::uuid> compute_visible_party_ids(const std::string& tenant_id, const boost::uuids::uuid& root_id) const { const auto snap = cache_.snapshot(tenant_id); if (!snap) return {root_id}; std::vector<boost::uuids::uuid> result; std::vector<boost::uuids::uuid> stack{root_id}; while (!stack.empty()) { const auto node = stack.back(); stack.pop_back(); result.push_back(node); const auto* siblings = snap->aux.find(node); if (siblings) for (const auto& child : *siblings) stack.push_back(child); } return result; }
8. See also
- ores.refdata — component group overview.