User Journey: Decide a publish
Table of Contents
This page documents a user journey: An approver holding the kind's decide permission reads a curve-publish request, weighs the change and the checks, and approves, refuses or holds it with a comment.
A user journey describes what one person is trying to do, before anything is built. It is a design document that outlives any sprint, so it names the screens, the entity fields each screen shows, and the operations and messages each step needs.
1. Goal
"I am the second pair of eyes. I want to see what this curve was before and what it would become, and to say yes, no or not yet, on the record." The journey is done when the request carries the approver's decision and a comment, and the request's state shows what that decision did to it.
2. Actor and trigger
The actor is the approver: the checker, an account other than the one that
raised the request, holding the permission the request's kind names in
decide_permission_code. The trigger is a request that is waiting. The journey
starts at the approval queue and ends when the request is approved, refused or
held, or when the approver leaves it for somebody else to decide. It picks up
the request raised in
Ask for a publish to be approved;
an approved request is applied in
Publish the approved set.
How many approvals the request needs is a rule of its kind, not of this screen.
approval_kind.approvals_required is the count — zero, one or n — and
approval_kind.decide_permission_code says who may decide. Each approval must
come from a different person, so an n-approval request stays open until n
distinct people have approved it. A hold is available only when the kind's
allows_hold is true, and a comment on approval is required only when
comment_on_approve is true; a refusal always needs one.
A request waits on a person, so the queue is one paged read over the shared record, filtered to the kinds the approver may decide. Two people may open the same request; the second decision is refused by its version, and the screen says who decided.
3. Steps
- Open the queue. The approver opens Approvals and reads the requests waiting in the tenant, oldest first: the kind, the curve, how long it has waited, and its state. The read is scoped by the decide permission, so the approver sees only the kinds they may decide.
- Open one request. The approver opens a request. The system reads its
header — who asked, when, and why — and the kind's detail that names what is
asked: the curve configuration, the as-of instant and the party the request
authorises, read by the request's id. The kind
marketdata.curve_publishis seeded, so a curve publish appears in the queue of a person holdinginbox::approvals:decide_market_risk, which in Acme is the Market Risk function: the head of market risk of each office. - Read the delta against the last published. The approver reads the candidate points beside the curve the system last published, point by point, so the decision is about a change and not about a number. The published set is known — the snapshot read states its as-of, its publisher and the request that authorised it — but no read returns the difference against it; see What is missing. Until one does, the panel names the published as-of and leaves the delta empty.
- Read the QA and validation checks. The approver reads the checks that were run over the candidates — all pillars priced, discount factors monotonic, no arbitrage — with each result. No operation returns these; see What is missing. Until then the panel names the checks and marks each result unknown.
- Read the rule that decides this request. The system shows how many approvals the kind needs, the permission it takes to decide, and when the request expires. The approver sees whether they are one of several.
- Read the decisions so far. The system lists every decision already taken on the request, with who took it, when, and the comment. An approval by the asker is refused, so a request cannot be waved through by the person who raised it.
- Decide. The approver chooses Approve, Refuse or Hold and writes a
comment. A refusal always needs the comment, because the asker reads it. The
system records the decision against the request and moves the request to the
state the decisions reach: an approval closes it only once
approvals_requiredapprovals stand, a hold parks it, and a refusal closes it. - Leave. The approver returns to the queue. The request is gone from the waiting list if it is final, and still there, held, if it is not. The system tells the asker what was decided; nothing does that today.
Step 7 is the step whose effect outlives the screen: the decision is written down, and the component that owns the kind applies it later. Approving this request does not itself publish a curve.
4. Screens and wireframes
One screen. The queue sits at the top, the selected request and the change it proposes sit in the middle, and the checks, the decisions so far and the decision panel sit at the bottom. The approver reads what is asked and what would change before the answer controls, so the act of deciding comes last.
Figure 1: Decide a publish: the tenant's waiting requests, one request with its proposed change and checks, and the decision panel.
The queue is the market-data approval screen, beside the curve screens rather than inside one curve, because the approver's work is the tenant's requests and not one curve's points.
5. Prototype
A static page at doc/prototypes/market_data/decide-a-publish/index.html: mock data only, no backend,
no build step and no session. It argues that the checker decides from the delta and the checks, with a comment, and the kind may ask for more than one pair of eyes.
- Variants
- a the queue beside the request, b the request first, c a group inbox
- States
- waiting, held, decided, multi (two approvals required, one given)
- Source
doc/prototypes/market_data/decide-a-publish/index.html- Open
/OreStudio/doc/prototypes/market_data/decide-a-publish/once the site is built, or the file directly
6. Entities composed
| Entity | What it contributes | Model |
|---|---|---|
approval_request |
The request under decision: who asked, which kind, why, when it expires, and its state | ores.inbox.approval_request |
approval_kind |
How many approvals are needed, the permission to decide, whether a hold is allowed, whether an approval needs a comment, and the expiry | ores.inbox.approval_kind |
approval_decision |
Each decision taken on the request, with who, when and the comment | ores.inbox.approval_decision |
approval_decision_type |
The decisions on offer — approve, refuse, hold, resume — and which need a comment | ores.inbox.approval_decision_type |
approval_request_state |
The state the request is in, and which states are final | ores.inbox.approval_request_state |
market_series |
The curve the request is about, and how it was derived | ores.marketdata.market_series |
market_observation |
The candidate points and the published points the delta compares | ores.marketdata.market_observation |
observation_lineage |
The recipe and source as-of behind each derived point, so the approver sees where the change came from | ores.marketdata.observation_lineage |
curve_publish |
The subject the request authorises: the curve configuration, the as-of and the party | ores.marketdata.curve_publish |
The subject the request is about is the kind's detail row, keyed by the
request's id: a curve_publish names the curve configuration, the series its
output is written to, the as-of instant, and the party the configuration
belongs to. Once a person applies the approved request, the same row names the
publisher and the instant, so the decider reads what a published set stands on
afterwards as well as before.
7. Operations and messages
| Step | Operation | Subject | Status |
|---|---|---|---|
| Open the queue | List the requests this approver may decide | inbox.v1.ops.list_approval_queue |
exists |
| Decide | Read the reasons a hold may name | inbox.v1.hold_reasons.list |
exists |
| Open one request | Read the request header | inbox.v1.approval_requests.get |
exists |
| Open one request | Read the requests of one kind | inbox.v1.approval_requests.list_by_kind_code |
exists |
| Open one request | Read the subject the request authorises | marketdata.v1.curve_publishes.list_by_request_id |
exists |
| Read the delta | Show the change against the published version | marketdata.v1.curve_candidates.diff |
missing |
| Read the checks | Run and return the QA checks over the candidates | marketdata.v1.curve_bootstrap.validate |
missing |
| Read the rule | Read how many approvals and who decides | inbox.v1.approval_kinds.list |
exists |
| Read the decisions so far | List the decisions on the request | inbox.v1.approval_decisions.list_by_request_id |
exists |
| Decide | Approve, refuse, hold or resume | inbox.v1.ops.decide_approval |
exists |
| Leave | Read the curve the request is about | marketdata.v1.market_series.get |
exists |
| Leave | Read the candidate points | marketdata.v1.market_observations.list_by_series_id |
exists |
The queue, the read and the decision are generic: they are defined in ores.inbox.approval_operations. A kind is what the queue and the decision are filtered by, and the subject a decider reads is the kind's own read, in the component that owns the kind.
8. What is missing
- The queue row shows no curve. The kind is seeded with
inbox::approvals:decide_market_riskas its decide permission, one approval, a hold allowed and a two-week window, so a Market Risk decider finds the request. The queue is one generic read over the shared header, and a header carries no subject, so the row shows the kind, the reason and the state. A decider who opens the request reads its subject on the kind's own read; the queue does not join to it. The candidate is a queue read that carries each kind's subject columns, which the model states as the kind's detail table. - There is no delta against the last published. A publish record marks the
set in use and names the as-of it was built at, but no read returns the
difference between it and the candidates. The candidate is a read such as
marketdata.v1.curve_candidates.diff, taking the published as-of from the record. Until then the delta panel is shown empty, with the reason stated, and the approver reads the published as-of beside the candidates rather than a change. - There is no QA or validation check operation. No subject runs the checks
over a candidate set and returns the results; the fit checks the desk
speaks of are not an operation. The candidate is
marketdata.v1.curve_bootstrap.validate, returning one row per check. Until then the panel names the checks and marks each result unknown, so the approver reads it as "not checked" and not as "passed". - The queue does not read any kind's detail. An approval request is a
header; the kind keeps its detail in a table of its own, keyed by the
request's id, in the component that owns the kind. A curve publish's detail
exists —
curve_publish, keyed byrequest_id— and the decider reads it by request when they open one. What no read does is join a page of the queue to each kind's detail, so the queue row has no curve in it. The candidate is a queue read that takes its subject columns from the kind's own detail table. - The deciding group is not a field.
approval_kindnames a permission code, not a group; which people may decide follows from who holds the code. The screen cannot say "Market data approvers" from the model. The candidate is a group named by the kind, or a permission bound to a group. Until then the screen shows the permission code and does not claim a group name. - A hold names a reason from a lookup. A curve publish allows a hold, because the approver's third answer is "not yet", so the hold takes a reason code from the hold reason lookup and never free text alone. The lookup is one list of the reasons the operational authorisation queue names, and a code it does not hold is refused with the codes it does.
9. The decision
9.1. The maker and checker pair on the kind
9.1.1. The rule
The kind names two permissions, and each one governs one half of the act:
| Field | What it permits | Seed in Acme |
|---|---|---|
raise_permission_code |
Raise a request of this kind: the maker's right | marketdata::curves:publish, held by the market data maker roles |
decide_permission_code |
Decide a request of this kind: the checker's right | inbox::approvals:decide_market_risk, held by the heads of market risk |
decide_permission_code exists today. raise_permission_code does not, and the
decision is that the kind gains it.
The four-eyes rule is a rule of the record, not of the pair. The decision insert
trigger refuses a decision whose decider is the request's raiser, and the service
answers four_eyes with the words "You asked for this request, so someone else
decides it." The pair makes the rule reachable in the ordinary case: the maker
holds the raise and not the decide, and the checker holds the decide. A tenant
that grants one account both permissions has a weaker control, and the record
still refuses the self-decision.
The queue is filtered by decide_permission_code, and the raise is refused to an
account without raise_permission_code. Neither permission appears on a screen:
a screen draws the action or leaves it out, and the access mark says Editable or
Read only.
9.1.2. Per kind, and not per request
The pair belongs to the kind. A request cannot name a permission of its own, and must not: a maker who could name a weaker checker would choose their own control. A kind may name parts, and each part names a permission and an answer order; the parts are per request, because a request names which functions answer it. The pair is not a part, and one request never widens it.
The permissions are held by an account in the tenant, never by a party. A curve publish for one party is therefore raised by a maker who holds the raise and decided by the tenant's Market Risk, and the subject names the party so the checker knows whose curve it is.
9.1.3. Why this answer
The record already enforces the separation that matters: the decider is not the asker. What the pair adds is the ordinary case, where the desk cannot answer its own request and the approver cannot raise one in the desk's name. One column on the kind and one check on the raise give that, and no screen changes.
9.1.4. The alternative
A request names the permission its decider must hold, and the kind names only a default. A maker then chooses the checker from a list the kind allows. This is the answer if a tenant needs a request to route to the functions of the party whose curve it is. It is rejected for now, because the queue would become a join over the requests and their named permissions, and the four-eyes rule would be the only fixed part of the control.
9.1.5. Open questions
- What is the raise permission called? The recommendation is
marketdata::curves:publish, in the market data component's namespace, because the act is the component's; the decide stays in the inbox's namespace because the inbox owns the queue. The alternative isinbox::approvals:raise_marketdata, which groups both halves under the queue. - Is the raise permission per curve family or per curve kind? The recommendation is per curve kind first: one code for an IR curve and one for an FX curve, granted to the maker roles that own each. The alternative is one code for every curve, which a desk that owns one family does not need.
- Is the pair declared on the kind or in a policy? The recommendation is on the
kind, beside
approvals_required, because the kind is already the one place the count, the hold and the expiry are read. The alternative is the policy model the book changes use, and it would split one control over two mechanisms. - What happens when an account holds both? The recommendation is that the seed never grants both to one role, and that the request page says plainly that the person may decide, so a tenant that grants both can see what it has done. The alternative is to refuse the grant, which the IAM model has no rule for.
10. Related journeys
- Ask for a publish to be approved — the request this screen decides.
- Publish the approved set — what an approval leads to, once the owning component applies it.
- Check a curve's fit — the review whose result the approver wants before deciding; the panel that would carry it is the QA checks here.
- Bootstrap or refresh a curve — where the candidates being decided were built.
- Withdraw or replace a published curve — the other market-data request an approver decides, after a curve is already published.
- See who changed a curve, and when — the trail of the decisions this screen writes.
11. Related knowledge
- Approval requests — the record, the lifecycle, four-eyes, who may decide, comments and expiry
- Interest Rate Curves
- Funding and Projection Curves
- Curve Point Provenance
- Term Structures and Tenors
- Tenor and Curve Bootstrapping Architecture
- Operational Authorisation — why a decision is metadata, and the four-eyes principle
12. See also
- Bootstrapping a curve: from quotes to a published set — the structure note that orders this cluster, and where to read this page in it.
- The market data path: a public fixing, a curve, a report — the structure note that orders this cluster, and where to read this page in it.
- Market data journeys — the group hub
- User Journeys
- ores.inbox.approval_request — the request under decision
- ores.inbox.approval_kind — the count, the permission and the rules this screen reads
- ores.inbox.approval_decision_type — the decisions on offer
- ores.inbox.approval_decision — the decision this journey writes