Task: Book a trade in one write
Table of Contents
This page documents a task in the Split the trade into an anchor and its components story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A caller books a trade through one service operation that writes the anchor, its booking and its state atomically, and a duplicate trade id comes back as a conflict, not a raw unique-key violation.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Split the trade into an anchor and its components |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-03 |
3. Acceptance
- One operation writes the anchor, the booking and the state in one transaction; a failure in any of them writes none.
- A second booking of the same trade id surfaces as a domain conflict.
- Build, ctest, pgTAP and the local check set pass.
4. Plan
- An operation model,
ores.trading.trade_operations, generates thebook_trade_requestandbook_trade_responsemessages ontrading.v1.trades.book. The handler and the service are hand-written, because an operation model generates neither. ores_trading_book_trade_fnwrites the anchor, the booking and the state in one statement, so a failure in any of them writes none. The anchor insert useson conflict do nothing: a trade id already booked writes nothing and returns false, which the service answers as a conflict with codealready_exists. The function runs as the caller, so grants and row-level security apply.- The service stamps the tenant, party and audit fields from the caller's context, so the anchor's party is always the caller's.
- The handler requires
trading::trades:write, the permission the wide trade's writers already hold.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
6. Notes
- Review of PR 2573 (booking and state), recorded here because the merge took
no further commit: (1) run the full ctest, done after the merge, 90/90; (2) a
booking written without a state leaves an actual trade in a virtual book with
no status, so consider a commit-time check: declined, because every
generated write test seeds an anchor through its own committed write, so the
check would break each of them; this operation is the sanctioned path, and
it always writes both; (3) the generated booking trigger checks the book's
existence and then its pin, which covers it: declined, a generator concern
outside the story; (4) the security-definer helpers take a tenant argument:
declined, the same pattern as the reporting helpers, called only from
triggers with the validated tenant; (5) the purger's
alter tabletakes a strong lock: noted, the existing hard-delete pattern; (6) netting sets read from ORE name no parties: recorded in the story's Decisions.
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2576 | [trading] Book a trade in one write |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Local review: a check the trade fails (an unknown book, a virtual book for a live trade) answers failed with internal_error rather than invalid |
trade_operations_handler.hpp |
Decline for now | The store's message names the cause; mapping SQLSTATEs to outcomes is a cross-cutting change no service does yet. |
| 2 | Local review: the operation books under trading::trades:write, not a permission of its own |
trade_operations_handler.hpp |
Accept as is | Booking writes trades; the wide trade's writers already hold it. |
| 3 | @claude: the raw database message reaches the client | trade_operations_handler.hpp |
Decline | The generated handlers do the same, and the trigger messages are the domain explanation the caller needs. |
| 4 | @claude: an empty result is reported as a conflict | trade_operations_service.cpp |
Accept | Only false is a conflict; any other result throws and is reported as failed. |
| 5 | @claude: empty change reason or commentary | trade_operations_service.cpp |
Decline | stamp fills the change reason when it is empty, and commentary is free text. |
| 6 | @claude: cover the optional casts, another party, the permission | service tests | Accept in part | The booking test now sends an execution timestamp and reads it back; the party comes from the context by construction, and the permission check is the shared helper. |
| 7 | @claude: a helper for the registrar block; a header-local logger | registrar.cpp, handler |
Decline | Both follow the existing hand-written handler pattern. |
10. Result
book_tradeontrading.v1.trades.bookbooks a trade: the anchor, the booking and the first state in one statement throughores_trading_book_trade_fn, so a failure in any of them writes none.- A trade id already booked writes nothing and answers a conflict with code
already_exists. - Tenant, party and audit fields come from the caller's context.
- Verified: build clean (
linux-clang-debug-make); ctest 90/90, including the three new service tests (books all three rows, a second booking is a conflict, a failing booking writes no anchor); db recreate clean; pgTAP 40/40; codegen pytest 1120; drift shows only the knowntrade_service.cppdiff; sweep, paste blocks, reachability, permissions, populate references, CMake, SQL hygiene, lint, structure notes, misspell and site page clean.