An instrument cannot be saved through the trading service

Table of Contents

This page is a capture in the inbox bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.

1. What

Saving an instrument through the trading service fails on the store's audit check: modified_by cannot be null or empty. Every instrument type fails, and it is not a data problem.

The generated write structs carry the business fields and no audit. equity_forward_instrument_write, for instance, holds instrument_id, trade_type_code, trade_id, the terms, and description – and no modified_by, performed_by, change_reason_code or change_commentary. The handler maps the write struct into the domain, so the domain's audit is empty, and the mapper passes that emptiness to the entity. The insert trigger then refuses the row.

Nothing stamps it. grep modified_by projects/ores.trading/core/src/messaging/*.cpp returns nothing: no trading handler sets an audit field, for any entity.

2. Why

The store's validator is explicit that the application must set the field: ores_iam_validate_account_username_fn raises "Application code must set this field explicitly" once the tenant has a real account, and it accepts an empty value only while the tenant is still in bootstrap. Trades survive because the ORE mapper fills their audit; instruments do not, because the DTO they travel in has nowhere to put one.

It went unnoticed because instruments are created by the SQL seeds, which set the audit columns directly and never go through the service. The ORE import is the first caller to write an instrument through NATS, and it fails on all 46 trades of a sample portfolio – vanilla_swap, equity_option, equity_forward, cap_floor, swaption, fx_vanilla_option, fx_forward, bond and credit repositories all report it.

Two coherent fixes, and the choice is architectural.

  1. Give the write structs an audit and have each handler pass it through. That is the shape credit_instrument, commodity_instrument, composite_instrument_data and scripted_instrument already use, where the handler assigns the whole domain object (req.data = r) rather than copying field by field. It is a codegen change and it regenerates the whole trading instrument protocol surface.
  2. Have the handler stamp the audit from the caller it is acting for, which is what ores.workflow's actor_from_message does for a workflow run: verify the forwarded token, take the username, fall back to the service account. This keeps the DTO free of audit, which is the more defensible boundary – the actor is a property of the request, not of the payload – but it puts the stamp in every generated handler.

The second is the better boundary and the first is the smaller change. Neither belongs in a mop-up: the trading instrument protocols are being regenerated by Sync the ores.trading tree to its models, so a change here would collide with it.

3. References

  • projects/ores.trading/api/include/ores.trading.api/messaging/equity_forward_instrument_protocol.hpp:42 — the write struct with no audit
  • projects/ores.trading/core/src/repository/vanilla_swap_instrument_mapper.cpp:84 — r.modified_by = v.audit.modified_by, which passes the emptiness on
  • projects/ores.sql/create/iam/iam_tenant_functions_create.sql:392 — the validator that refuses it
  • projects/ores.ore/service/src/messaging/ore_import_execute_handler.cpp:1411 — the swap branch that copies field by field and never the audit
  • projects/ores.workflow/core/include/ores.workflow.core/service/workflow_actor.hpp:32 — actor_from_message, the existing way to resolve a caller

4. See also

Emacs 29.3 (Org mode 9.6.15)