# Finance Desk — payment operations

A payments desk for a platform, built as a template on the Reckon design system.
Product: **Meridian**; workspace **Northwind Markets**. Twenty-nine screens
across thirty registered routes, one fixture set, one route table. Money is
authored in **cents** everywhere.

Open `FinanceDesk.dc.html`. The floating page menu picks the screen and the
theme.

## The job it models

Somebody in treasury operations watches the money moving through a platform:
what came in, what is waiting to go out, what the company itself spent on its
own cards, what a customer is disputing, and how much of the balance is actually
theirs to move today.

## Files

| File | Holds |
|---|---|
| `FinanceDesk.dc.html` | The template entry. Loads `ds-base.js`, `kit.css`, `boot.js`. |
| `ds-base.js` | Loads this design system. One line to repoint in a consuming project. |
| `boot.js` | Loads the product's files in order and publishes `window.MeridianApp`. |
| `enums.js` | Three status fields, the classification lists, the derived flags, the transitions. |
| `nav.js` | The route table and the projections the shell renders. |
| `fixtures.js` | 5 people, 13 customers, 20 payments, 8 payouts, 3 disputes, 6 cards, 22 card expenses, one balance, 6 notifications, 10 notes. The only data in the product. |
| `metrics.js` | Every figure any screen renders. Nothing here holds data. |
| `kit.css` | Layout only. Every colour, radius, shadow and step is a token. |
| `lib/` | `contracts.js`, `ui-runtime.js`, `command-palette.js` — copied in, domain-free. |
| `MeridianShell.jsx` | The sidebar shell and the page header. |
| `MeridianParts.jsx` | The product's local components (below). |
| `Social.jsx` | Faces, the note thread, the mentions queue. |
| `CustomersScreens.jsx` | The customer list, one account, and onboarding one. |
| `PeopleScreens.jsx` | The people canvas and one person's page. |
| `ActivityScreen.jsx` | The derived activity feed. |
| `NewMoneyScreens.jsx` | Taking a payment and raising a payout. |
| `Charts.jsx` | `DayChart` and `MixRows` — the product's two chart primitives. |
| `DashboardCards.jsx` / `DashboardScreen.jsx` | The dashboard and its seven cards. |
| `*Screen.jsx` | The other screens. |
| `AccountScreens.jsx` | Profile, workspace settings, notification preferences. |
| `AuthScreens.jsx` | Sign in, the 2FA challenge, code sign-in, forgot and reset password, signed out. |
| `SettingsScreens.jsx` | Spend controls (the policy the derived flags read) and team and approvals. |
| `App.jsx` | Routing plus every commit in the product. |

## The screens

| Route | Screen | Primary display | Commit model |
|---|---|---|---|
| `/` | Dashboard | Figure row over a volume chart and four actionable cards | Instant |
| `/payments` | Payments | Fixed-window volume chart over the table | Instant |
| `/payments/new` | New payment | Form beside a live fee preview | **Staged** |
| `/payouts` | Payouts | Figure row over the approval queue | Instant |
| `/payouts/new` | New payout | Form beside the balance chain with the draft taken out | **Staged** |
| `/expenses` | Expenses | Scoped figures and a spend chart over the review table | Instant |
| `/cards` | Cards | A canvas of card tiles, each with its limit meter | Instant |
| `/cards/:id` | Card | Spend chart, the card's facts, its whole ledger, its thread | Instant |
| `/disputes` | Disputes | List-detail; `/disputes/:id` is the selection, with its thread | Instant |
| `/customers` | Customers | Table ordered by volume, over four figures | Instant |
| `/customers/new` | New customer | Form beside a preview of what it creates | **Staged** |
| `/customers/:id` | Customer | Their figures, payments, payouts, disputes and thread | Instant |
| `/people` | People | A canvas of person tiles, faces at a readable size | Instant |
| `/people/:id` | Person | Their spend, cards, approvals, accounts and thread | Instant |
| `/activity` | Activity | A feed derived from the records, with the person who did it | Instant |
| `/balances` | Balances | The balance in three parts, over the movement chart | Instant |
| `/reports` | Reports | Chart-led: money in and out by day, then the mixes | Instant |
| `/settings` | Workspace settings | Form | **Staged** |
| `/settings/profile` | Your profile | Form | **Staged** |
| `/settings/spend-controls` | Spend controls | Form, with a live preview of the draft policy | **Staged** |
| `/settings/team` | Team and approvals | Form in a table: role, limit, card issuing | **Staged** |
| `/settings/notifications` | Notification preferences | Form | **Staged** |
| `/settings/billing` | Plan and billing | Table of the next bill, every line checkable | Instant |
| `/signin` | Sign in | Two-panel: form left, brand plane right | **Staged** |
| `/two-factor` | Two-factor challenge | `OTPField`, with a recovery-code path | **Staged** |
| `/verify-code` | Sign in with a code | Email, then `OTPField` with a resend clock | **Staged** |
| `/forgot-password` | Forgot password | Form, then a sent state that changes nothing | **Staged** |
| `/reset-password` | Set a new password | Form with the requirements met as you type | **Staged** |
| `/signout` | Signed out | Whose session ended, and one way back | **Staged** |

Eleven destinations in **two labelled groups** — Money (dashboard, payments,
payouts, disputes, balances, reports) and Workspace (expenses, cards, customers,
people, activity) — with settings, the profile and auth registered off the
sidebar and reached from the account menu.

## Where records get made

Every entity the product lists has the action that creates it (§5). A customer,
a payment and a payout are real forms, so each is a registered route with a
live preview beside it: `/customers/new` prints what it will create,
`/payments/new` prices the charge through `methodFee` — the same function the
billing screen bills with — and `/payouts/new` runs `balanceBreakdown` with the
drafted amount taken out of it. Issuing a card and inviting a teammate are two
and three fields, so both are staged dialogs opened from an instant screen. A
dispute has no create action, because a chargeback arrives from the card network
and never from this desk.

Entitlements gate the controls rather than the copy: a suspended or closed
customer is not in the new-payment options, an account with no bank details is
not in the new-payout options, and each field says how many accounts were left
out and why.

## Auth

Six screens in `AuthScreens.jsx`, sharing one shell and nothing with the rest of
the product: **two panels**, the form on the left where reading starts and the
brand plane on the right in `--primary` / `--primary-foreground` (so it inverts
correctly in dark mode with no second colour invented), carrying one line per
screen about the step. The dot art is a single tiled `radial-gradient` faded by a
`mask-image` — nothing is drawn per dot. Below 900px the brand panel is gone
rather than stacked: on a phone the form is the screen.

One flow: `/signin` → `/two-factor` → the product. A forgotten password goes
`/forgot-password` → `/reset-password`; somebody who would rather not type one
goes `/verify-code`. Every screen is registered, because a person arrives at a
reset or code link cold. Both code screens use the design system's own `OTPField`
rather than a hand-rolled row of inputs, and the reset screen shows its three
requirements being met rather than listing them and then failing them.

## Decisions taken once, for the whole product

- **Shell**: sidebar, one navigation owner, two labelled groups — Money and
  Workspace, because eleven destinations in one group is outside §6.3's 5–9.
  Cards is the destination that would most plausibly have been a table
  and is not: it answers "how much of this limit is left", which a meter shows
  and a row does not. Balances is the same argument one level up.
- **Density**: comfortable — 32px nav rows, 16px page gutters, 44px table rows.
- **Separation**: hairlines on the tinted `--sidebar` plane; cards are the only
  raised surface, one elevation step, no nesting.
- **Active state**: `--sidebar-accent` wash + foreground token + medium weight.
- **Recession**: a payout nobody here can act on — with the network, or already
  closed — is recessed. A *held* payout is not: it is waiting on a person, and
  the row names the team and the reason.
- **List architecture**, identical on payments and payouts: filter bar, bulk
  action bar, table, stated count.
- **Rows carry the transition their status asks for** — the primary one, as a
  button. Everything else, destructive or merely secondary, lives in the row's
  menu: a list is not where you put a button that cannot be undone, and a second
  button competing with the primary one is one entry point too many.
- **Charts**: CSS bars, one bucket a day, from `metrics.js`. Every bar carries
  a tooltip naming the day, the figure and how many rows it summed. Two series
  at most, and the words beside the chart say what is outside it.

## The money chain

Nothing on a screen subtracts anything; `metrics.js` owns it:

```
available (stored, the only stored money figure)
  + committed = Σ payouts that have not landed
  + reserved  = Σ open disputes
  = on the platform
```

`balanceBreakdown` is read by the dashboard's balance card and by the balances
screen, and the two tables on that screen are the arrays it summed.

## Act-ability

Two predicates, one per queue. `PAYOUT_FLAGS.isApprovable` — awaiting approval,
nothing in the way, **and** inside the approver's own limit — is read by the
sidebar badge, the dashboard queue, the payouts figure, the row's action and the
bulk bar alike, each handed the same signed-in approver. A held payout is never
approvable and the row says who is holding it; one above your limit is never
approvable either, and the row names the people who can release it. Approve is
present and disabled there rather than omitted, because the precondition is a
permission the user can see; Reject stays live, since turning a payout down
keeps the money and is not what the limit gates.
`EXPENSE_FLAGS.isReviewable` is the same shape for card spend: waiting on us
**and** its receipt policy met. The expenses badge, the four tiles, the
dashboard card, the row's approve button and the bulk bar all call it, and an
expense still owed a receipt is recessed with the person's name on it.

## Policy

`MERIDIAN_POLICY_DEFAULTS` holds the four numbers the derived flags read: the
receipt threshold, the categories that always need a receipt, the share of a
card's limit that counts as "near", and the default limit for a new card. It
lives beside the enums, never inside a predicate, and every predicate takes it
as an argument — so `/settings/spend-controls` can run the real evaluator over
the real expenses with the *draft* values and print what would change, and
saving really does recount the sidebar badge.

## Verbs, named once

Approve payout · Reject payout · New payout · Capture payment · Retry payment ·
Refund payment · New payment · Approve expense · Query expense · Deny expense ·
New card · Freeze card · Unfreeze card · Cancel card · Submit evidence · Accept
dispute · New customer · Put on hold · Resume payouts · Close account · Invite
teammate · Post note · Save profile · Save workspace · Save spend controls ·
Save team · Save notification preferences · Export · Clear filters · Sign in ·
Sign out.

Irreversible (confirm dialog): Reject payout, Refund payment, Deny expense,
Cancel card, Accept dispute, Close account. Consequential and reversible (undo
toast): everything else, including every create and every note.

## Local components — reported, per §9

- **`Transitions` / `RowActions`** — draw a transition set from `enums.js`;
  `RowActions` keeps destructive transitions in a row menu.
- **`FilterBar` / `BulkBar`** — the list-screen furniture, identical on both
  list screens.
- **`FormDialog`** — a staged dialog, so an instant screen can collect two
  fields without becoming staged.
- **`FormBar`** — the staged-form footer every settings screen ends with.
- **`DayChart`** (`Charts.jsx`) — one column a day, one or two series, a tooltip
  per bucket. Used by the dashboard, payments, expenses, a card, balances and
  reports; nothing else in the product draws a chart.
- **`MixRows`** (`Charts.jsx`) — the proportion list behind "how the volume
  arrived" and "where the spend went", both of which decompose the figure
  printed above them.
- **`LimitMeter`** (`CardsScreen.jsx`) — a card's spend against its rolling
  30-day limit, over and near states included.
- **`MetricTile`**, **`ExportMenu`**, **`PageHeader`**, **`RouteNotBuilt`**.
- **`Face` / `FaceStack` / `MentionText` / `ThreadCard` / `ReplyQueueCard`**
  (`Social.jsx`) — the social layer. `ThreadCard` is the same component on a
  dispute and on a card; `ReplyQueueCard` is the dashboard's read of the one
  awaiting-reply predicate.
- **`Floating` / `FloatingTip`** (in `lib/ui-runtime.js`) — portalled menu,
  popover and tooltip. The design system's own overlays now portal too; these stay
  for their different API (one element as the trigger, a render callback that
  receives `close`, no compound Trigger/Popup pair).

## Coverage

`dispute.status: won` has no fixture and no action that reaches it — a win comes
from the network, not from this desk. `lost` is reached by accepting a dispute,
and `under_review` by submitting evidence. Every value of `customer.status` has
a fixture and a reason the rest of the data agrees with (Vantage on hold while
dsp_2XB runs, Sable & Finch suspended after po_7725 came back, Ridgeway closed),
and three of the four are reachable by an action. Every other value of every
field appears at least once in `fixtures.js`.

## Deviations, and why

- **A customer is joined by name.** A payment, payout and dispute each carry the
  customer's name, and `CUSTOMERS` is the record that name refers to — so the
  name is written once per company and every screen looks the account up by it
  (`customerByName`). An id on every money row would be tidier and is the
  obvious next refactor; the join key being the displayed string is the one
  place this template trades purity for not restating 31 rows.

- **`.js`, not `.ts`.** This kit runs unbundled in a browser, so `enums.ts` and
  `nav.ts` are `enums.js` and `nav.js`. Same contents, same rules.
- **`lib/contracts.js` is one file**, not the two `lib/data/` files — the same
  contract surface, scoped to the product folder so it dies with it.
- **No per-record screens for payments and payouts.** Notifications and palette
  records land on the list that holds them rather than on a route with nothing
  behind it. A dispute is the exception: the disputes screen is list-detail, so
  `/disputes/:id` is a real route.
- **Customers and developer tools are still absent from the route table.** They
  are separate products in the same suite, and a sidebar that advertises
  destinations with nothing behind them is the failure this template was written
  to avoid.
- **A card limit is per rolling 30 days, not per calendar month.** A monthly
  limit reads empty for the first days of a month, which makes every meter on
  the cards screen lie about the same fixture on different days. One window,
  `CARD_WINDOW_DAYS`, named once and stated in words beside every figure it
  scopes.
- **`assets/providers/` is absent**, so the auth screens offer password only.

## Known gaps

- Pagination: 20 payments, 22 expenses and 8 payouts do not need it, so the list
  screens end in a stated count rather than a `Pagination`. A real platform
  would need it.
- Responsive behaviour is declared once in `kit.css` (1280 / 1024 / 900 / 767).
  Verified at 1400 and at drawer width; the 375px end wants a device check.

## The social layer

`Social.jsx` adds the people to a product that only showed money. Threads live
on the four record types that have a screen — a dispute, a card, a customer and
a person — and faces appear wherever a record names somebody: the sidebar
account, the customer's owner, the card's holder, the note's author, the
activity row's actor, the signed-out screen.

- **Notes on a record.** `COMMENTS` in `fixtures.js` is the only social data.
  Notes exist on the two record types that have a screen — a dispute and a card
  — so a mention always opens onto its own thread rather than a list where the
  conversation is invisible. A mention in a body is a **member id**
  (`@usr_tomas`), resolved through `MEMBERS` at render, so a person's name is
  written once in the product.
- **No `comment.status`.** A note has no lifecycle. The only question a screen
  asks — is a reply still owed — is derived: `COMMENT_FLAGS.isAwaitingReply` is
  true when the person mentioned has not written on that record since. The note's
  own badge, the thread footer, the dashboard queue and the palette group all
  call it.
- **Faces are derived.** `peopleOn` reads the record's own person fields plus its
  thread; there is no watcher list, so nobody appears who has not touched the
  record. `FaceStack` overlaps at most four and names the rest on hover.
- **`ReplyQueueCard`** on the dashboard lists the notes mentioning you that you
  have not answered, each opening the record. **Posting** is consequential and
  reversible: it applies immediately and raises an undo toast, and the mentions
  are read out of the text rather than collected by a second control.

## Added in this pass

- **Four new auth screens** — the 2FA challenge, code sign-in, forgot password
  and set-a-new-password — with the auth shell rebuilt as two responsive panels,
  the brand plane on the right. Auth moved into its own `AuthScreens.jsx`.
- **Eight new pages**: customers, one customer, onboarding one, people, one
  person, the activity feed, and the two money creates. Every one of them has a
  route, a create path where an entity needed one, and faces where a record
  names a person.
- **The sidebar regrouped** into Money and Workspace, because eleven
  destinations in one group is outside §6.3's 5–9.
- **A brand mark** (`assets/templates/logos/15-logo-demo.svg`) in the sidebar
  brand block and above the auth form, replacing the letter tile.
- **Auth polish**: the password field is an `InputGroup` with a reveal toggle,
  and the signed-out screen shows whose session ended.
- **The preview no longer carries tweaks.** `startPath` and `theme` are gone from
  the DC props: the floating page menu owns page choice and the theme.
- **`methodFee` moved into `metrics.js`**, so the fee quoted before a charge and
  the fee billed after it are one function called twice.

## Added in an earlier pass

- **`/settings/billing` — plan and billing.** The plan was stored and shown nowhere. Every line of the next bill is `FEE_SCHEDULE` over the rows on the destination it names — processing per method, payouts that landed, disputes opened, seats and cards over the plan — so a fee can always be checked. No stored money figures.
- **Fixed product-wide:** `.fd-link` is a button, and a button in a flex column centres its own text, which read as an indent on every linked record name.
- **Preview page menu** (`lib/template-menu.js`, byte-identical across the kit).
