Refdata Party Domain: tenant_id Strong-Type Migration
Table of Contents
Problem
The ores.refdata.api domain contains 35 entity types. The majority already
carry utility::uuid::tenant_id tenant_id = utility::uuid::tenant_id::system();
(the strong type), but 11 party-related types still use std::string tenant_id.
This inconsistency:
- Allows silently assigning arbitrary strings (e.g.
"system") to a field that must be a valid UUID. - Was the root cause of a
bad access to std::expectedcrash inores.synthetic.coretests (2026-05-20): generators calledtenant_id::from_string("system").value()without a fallback. - Makes the domain model internally inconsistent:
portfoliohas the strong type;party(the entity that owns portfolios) does not.
The 11 unmigrated types are all in the party/counterparty cluster, which was
implemented before the strong type was introduced. Eventing and messaging
types intentionally keep std::string for serialisation; they are out of scope.
Scope
In scope — 11 domain types in ores.refdata.api/domain/
| File | Notes |
|---|---|
party.hpp |
Root entity; has special repo |
business_unit.hpp |
|
business_unit_type.hpp |
|
counterparty.hpp |
|
counterparty_identifier.hpp |
|
counterparty_contact_information.hpp |
|
party_contact_information.hpp |
|
party_counterparty.hpp |
|
party_country.hpp |
|
party_currency.hpp |
|
party_identifier.hpp |
Out of scope
- Eventing types under
ores.refdata.api/eventing/— intentionallystd::stringfor NATS/JSON serialisation. - Messaging protocol types under
ores.refdata.api/messaging/— same reason. - Any type outside
ores.refdata.api(IAM, DQ, reporting, etc.).
Migration Pattern
The fully migrated portfolio type is the canonical reference.
1. Domain header
- std::string tenant_id; + utility::uuid::tenant_id tenant_id = utility::uuid::tenant_id::system();
Add the include if not already present:
#include "ores.utility/uuid/tenant_id.hpp"
2. Repository mapper — entity→domain (read from DB)
The DB column always contains a valid UUID string. Use .value() — a malformed
row is a data corruption bug, not an expected runtime condition.
- r.tenant_id = v.tenant_id; + r.tenant_id = utility::uuid::tenant_id::from_string(v.tenant_id).value();
Reference: portfolio_mapper.cpp line 38.
3. Repository mapper — domain→entity (write to DB)
- r.tenant_id = v.tenant_id; + r.tenant_id = v.tenant_id.to_string();
Reference: portfolio_mapper.cpp line 68.
4. Special case: party_repository.cpp manual row mapping
party_repository::read_system_party maps rows by index rather than via the
mapper (line 129). Change:
- p.tenant_id = *row[1]; + p.tenant_id = utility::uuid::tenant_id::from_string(*row[1]).value();
5. Generators
Generators pull tenant_id from ctx.env() with a default of "system". The
literal "system" is not a UUID; use value_or to fall back to the system
tenant sentinel:
- r.tenant_id = tenant_id; + r.tenant_id = utility::uuid::tenant_id::from_string(tenant_id) + .value_or(utility::uuid::tenant_id::system());
Add the include if not already present:
#include "ores.utility/uuid/tenant_id.hpp"
Affected generators (all in ores.refdata.api/src/generators/):
party_generator.cppbusiness_unit_generator.cppbusiness_unit_type_generator.cppcounterparty_generator.cppcounterparty_identifier_generator.cppcounterparty_contact_information_generator.cppparty_contact_information_generator.cppparty_counterparty_generator.cppparty_country_generator.cppparty_currency_generator.cppparty_identifier_generator.cpp
Implementation Checklist
Domain headers
[X]party.hpp— change field type, add include[X]business_unit.hpp— change field type, add include[X]business_unit_type.hpp— change field type, add include[X]counterparty.hpp— change field type, add include[X]counterparty_identifier.hpp— change field type, add include[X]counterparty_contact_information.hpp— change field type, add include[X]party_contact_information.hpp— change field type, add include[X]party_counterparty.hpp— change field type, add include[X]party_country.hpp— change field type, add include[X]party_currency.hpp— change field type, add include[X]party_identifier.hpp— change field type, add include
Repository mappers (ores.refdata.core/src/repository/)
[X]party_mapper.cpp— both read and write sides[X]business_unit_mapper.cpp— both read and write sides[X]business_unit_type_mapper.cpp— both read and write sides[X]counterparty_mapper.cpp— both read and write sides[X]counterparty_identifier_mapper.cpp— both read and write sides[X]counterparty_contact_information_mapper.cpp— both read and write sides[X]party_contact_information_mapper.cpp— both read and write sides[X]party_counterparty_mapper.cpp— both read and write sides[X]party_country_mapper.cpp— both read and write sides[X]party_currency_mapper.cpp— both read and write sides[X]party_identifier_mapper.cpp— both read and write sides
Special repository cases
[X]party_repository.cpp— manual row mapping inread_system_party(line 129)
Generators (ores.refdata.api/src/generators/)
[X]party_generator.cpp[X]business_unit_generator.cpp[X]business_unit_type_generator.cpp[X]counterparty_generator.cpp[X]counterparty_identifier_generator.cpp[X]counterparty_contact_information_generator.cpp[X]party_contact_information_generator.cpp[X]party_counterparty_generator.cpp[X]party_country_generator.cpp[X]party_currency_generator.cpp[X]party_identifier_generator.cpp
Test files (ores.refdata.core/tests/ and ores.synthetic.core/tests/)
These were not in the original scope but required fixes to compile after the
domain header changes. The pattern in each: update find_system_party_id
helper to take const ores::utility::uuid::tenant_id&, remove spurious
.to_string() at call sites, and update make_pc / make_party_* helpers
that assigned h.tenant_id().to_string() to pc.tenant_id.
[X]party_rls_isolation_tests.cpp[X]repository_party_counterparty_repository_tests.cpp[X]repository_party_country_repository_tests.cpp[X]repository_party_currency_repository_tests.cpp[X]organisation_publisher_integration_tests.cpp(ores.synthetic.core) — movedfrom_string()conversion before the genericstamplambda so all entity types receive the strong type directly
Build and test
[X]Buildores.refdata.api.libandores.refdata.core.libcleanly[X]All refdata repository tests pass (ores.refdata.coretest suite)[X]ores.synthetic.coretests pass (no morebad access to std::expected)
Notes
- The mapper read side uses
.value()(not.value_or) because a malformed UUID in the DB is a data integrity bug. The generator uses.value_orbecause the generation context allows"system"as a human-readable shorthand. This shorthand is a leaky abstraction — cleanup is tracked in2026-05-20-refdata-cpp-codegen-activation.org(Notes section). - No SQL migration is needed. The DB column is already
uuid; only the C++ representation changes. - No eventing or messaging types are touched. Downstream consumers of
party_changed_eventetc. already handlestd::string tenant_idfrom the serialised form.