Skip to content

SPEC-152-Q4: Client-Facing Suitability Report Viewer

Capability: SUITABILITY-ENGINE · Status: draft Repo: suitability-engine

Audience: WTP engineers building/reviewing this; Origo’s own front-end team, who will build their own equivalent view against the JSON contract below (we do not ship this UI into their product — see Deployment model).

Origo needs their end-client to view and acknowledge a suitability report, matching the UX pattern already proven live in client-onboarding’s own client-facing report (apps/client-onboarding/src/app/report/) — a progressive-disclosure, section-by-section review that gates the acceptance action on having actually viewed every required section, with every transition captured as an audit event (the same pattern the real client-portal repo formalises for FCA acknowledgement). This spec documents that reference UX precisely enough for Origo to reproduce it (or diverge deliberately), and ships a working reference implementation proving the JSON contract is sufficient to drive it.

2. Reference UX (as built in client-onboarding, source of truth)

Section titled “2. Reference UX (as built in client-onboarding, source of truth)”
  • Fixed left sidebar (dark navy, wtp-navy-deep in the source brand palette — a placeholder in the reference implementation, see Branding), full viewport height, collapsible on mobile behind a hamburger toggle.

  • Report title + client name at the top of the sidebar (“Suitability Report” / “Jane Doe”).

  • Nine section links, each with an icon, in a fixed order:

    OrderSection idLabel
    1overviewOverview
    2about-youAbout You
    3objectivesYour Objectives
    4riskAttitude to Risk
    5recommendationsMy Recommendations
    6suitabilityWhy This is Suitable
    7costsCosts & Charges
    8risksKey Risks
    9downloadsDownloads
  • Each link shows a checkmark once viewed (teal accent), and the currently active section gets a left border + lighter background — pure client-side state, driven by navigation, not a separate “mark as read” button.

  • “Proceed to Acceptance” button, fixed at the bottom of the sidebar, disabled until every section in a required subset has been viewed (not necessarily all nine — client-onboarding’s REQUIRED_SECTIONS_FOR_ACCEPTANCE is a subset; Origo should decide their own required set, but the pattern — some sections may be “must-read”, others optional context — is worth keeping).

2.2 Section-view tracking = the audit trail

Section titled “2.2 Section-view tracking = the audit trail”

This is the part that matters most and is easy to under-build: viewing a section is itself the event being audited, not a side effect of a “mark read” click. In the reference implementation, navigating to a section fires a “viewed” event exactly once per section per session (idempotent — revisiting doesn’t re-fire). client-onboarding persists this server-side per client (reportSectionsViewed on the client record, patched via the API) so the gate on “Proceed to Acceptance” survives a page reload — this is not just client-side UI state, it’s the FCA-relevant record of what the client actually looked at before accepting.

For Origo: this means your own back end needs an equivalent durable “sections viewed” store per client engagement, and your acceptance/proceed action needs to check it server-side, not just trust client-side state — otherwise the audit trail is forgeable by anyone who can call your “accept” endpoint directly.

Each section is its own scrollable panel in the main content area. Representative example (suitability, the “Why This is Suitable” section):

  • A heading card explaining the recommended management approach (recommendation.dfmManager etc.) with a 3-up feature-benefit grid.
  • A performance-comparison visual (current vs. recommended platform).
  • A list of collapsible reason cards — each with a heading and an expandable explanation (accordion pattern, one open/closed state per card, independent of the others).

Other sections (About You, Objectives, Risk, Recommendations, Costs, Risks, Downloads) follow the same visual language — card-based panels, consistent spacing, no page-level scroll competing with section-level scroll.

2.4 Data mapping — SuitabilityData → the nine sections

Section titled “2.4 Data mapping — SuitabilityData → the nine sections”

The reference implementation renders directly off GET /v1/tenants/:tenantId/clients/:clientToken/suitability (or the format=json export route)‘s response, i.e. { data: SuitabilityData, audit } per suitability-template/contract.ts. Mapping:

SectionSource fields
overviewclient.fullName, report.generatedAt, firm.name, adviser.*
about-youclient.* (address, DOB, risk profile, investment experience)
objectivesclient.objectives.timeHorizonYears
riskclient.riskScore, client.riskRating, client.capacityForLoss
recommendationsrecommendedPlatform.*, recommendation.*
suitabilitysuitabilityReasons[] (one card each, when supplied), else recommendation.summaryStatement; recommendation.dfmManager
costscharges.*, currentPlatform.{amc,platformFee,totalCharges}
risks(integrator-authored — no contract field; see below)
downloadsreport.enclosures

suitabilityReasons — integrator-authored pass-through (suitability-engine#16, decided 2026-09-30: Option A). client-onboarding’s real suitability page renders a list of narrative reason cards (suitabilityReasons: [{id, heading, explanation}]) sourced from its own client record. SuitabilityData (contract.ts) now carries the same field as an optional pass-through:

  • The integrator (Origo included) supplies suitabilityReasons on the PartialSuitabilityData input (REST export body, GraphQL PartialSuitabilityDataInput/ClientCreateInput, gRPC suitability_reasons_json, client.create), from its own client record.

  • derive() copies it to the derived SuitabilityData unchanged. The engine does not author, rewrite, reorder or synthesise reasons — no rule in suitability-rules/ reads or writes it — and when none are supplied the field is simply absent (never an engine-invented default).

  • Every request surface validates it in full (id, heading, explanation all non-empty strings): malformed reasons are a 400 / BAD_USER_INPUT / field-level client.create error, because nothing downstream would otherwise stop a malformed card reaching a client.

  • Readiness (field-source-map.ts) classifies it Manual — integrator/adviser-authored content, like recommendation.summaryStatement.

  • Limits: at most 50 reasons; id up to 128, heading up to 500, explanation up to 10,000 characters; no keys beyond {id, heading, explanation}.

  • Rendering: reasons are a list, so they render through JSON consumers or a Liquid (config-mount) template’s {% for reason in suitabilityReasons %} loop. A DOCX template’s scalar {{...}} tags cannot render a list (the same constraint as report.enclosures) — do not place {{suitabilityReasons}} in a DOCX template. Heading/explanation are caller-supplied free text: a Liquid template emitting HTML must | escape them.

The reference implementation renders one card per supplied reason and falls back to a single card built from recommendation.summaryStatement when the integrator supplied none.

risks (Key Risks) remains integrator-authored, outside the contract. It is the same “the integrator writes the narrative” model, but it is not covered by this pass-through: there is no SuitabilityData field for it (client-onboarding’s version isn’t traced to a shared contract either), so the reference implementation leaves it as an Origo-authored static section. Adding a parallel optional pass-through for it would be a separate, deliberate contract change, not implied by #16.

The colour palette, typography, and logo in client-onboarding’s real report are WTP/Benchmark brand tokens — not part of what Origo integrates against. The JSON contract carries data, never presentation. The reference implementation uses a neutral placeholder palette (documented in its own README) specifically so it reads as “swap this,” not “this is WTP’s brand, keep it.”

3. Integration flow — how Origo actually calls this

Section titled “3. Integration flow — how Origo actually calls this”
  1. Origo’s back end calls POST /v1/tenants/{tenantId}/clients/{clientToken}/suitability/export?format=json (bearer: SUITABILITY_ENGINE_SERVER_TOKEN, or the Origo-facing API-key-with-scopes model once that ships — see docs/external/origo-suitability-and-adviser-api-design.md §5.1 in client-onboarding) to get { data: SuitabilityData, audit }.
  2. Origo’s front end renders it using their own equivalent of the shell described above (or the reference implementation as a starting point).
  3. Section-view events are recorded by Origo, in Origo’s own store — this engine has no concept of “has this client viewed this section”, and is not the right place to add one (it’s a stateless derive-and-render pipeline, not a session/audit-trail service).
  4. Origo’s own “accept” action is Origo’s own audit event, entirely outside this engine’s scope.
  5. If the branded document (not just the web view) is needed — e.g. for download/email — that’s the format=docx path (suitability-engine#6, now implemented) against Origo’s own registered template.

4. Deployment model — what WTP ships vs. what Origo builds

Section titled “4. Deployment model — what WTP ships vs. what Origo builds”

Per the scope decision made with the user 2026-08-16: WTP ships this spec + a reference implementation (examples/client-report-viewer/) that Origo can literally copy, fork, or just read for the interaction pattern. WTP does not operate, host, or maintain a production version of this UI for Origo — Origo’s own client-facing product is Origo’s to build and brand. The reference implementation is explicitly not embedded into client-onboarding or any other WTP product’s UX.

See examples/client-report-viewer/ — a standalone Vite + React app (no Next.js/WTP-app-shell coupling, so it’s genuinely portable). Run npm run dev inside that directory; it renders against a bundled sample SuitabilityData payload (examples/client-report-viewer/src/sample-data.ts) matching the shape a real API response would carry, so no live backend call is required to see it work. See that directory’s own README for architecture notes and exactly what is/isn’t implemented.