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

  1. 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.
  2. 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_publish is seeded, so a curve publish appears in the queue of a person holding inbox::approvals:decide_market_risk, which in Acme is the Market Risk function: the head of market risk of each office.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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_required approvals stand, a hold parks it, and a refusal closes it.
  8. 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.

wireframe_decide-a-publish.png

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

  1. The queue row shows no curve. The kind is seeded with inbox::approvals:decide_market_risk as 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.
  2. 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.
  3. 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".
  4. 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 by request_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.
  5. The deciding group is not a field. approval_kind names 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.
  6. 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

  1. 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 is inbox::approvals:raise_marketdata, which groups both halves under the queue.
  2. 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.
  3. 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.
  4. 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

11. Related knowledge

12. See also

Emacs 29.3 (Org mode 9.6.15)