Accountant Portal — Financial Reporting
Audiences: clinical-buyer, developer
The clinic finance lens — invoices, payments, revenue, expenses, and reports — separated from the clinic-admin's operations view by RBAC, but sharing the same role-portal factory and BFF gateway.
Business Purpose
Healthcare invoicing is a separate craft from clinical care: payer mix
reconciliation, statutory chart of accounts (Germany: SKR03/SKR04), VAT on
non-medical line items, deferred revenue, payout reports for the practice's
external bookkeeper. The accountant portal is the surface where the clinic's
finance staff (or an outsourced Steuerberater) sees the financial state of
the practice without seeing the patient record proper. It is the only
portal where a non-clinician can routinely log in to a clinical product — the
RBAC contract isolates them to ACCOUNTANT_ONLY routes.
For an investor narrative, this matters because it converts the HMS from a clinician tool into a back-office system of record — clinics that already have HMS-as-EMR can adopt HMS-as-finance-system instead of running parallel SaaS for billing. For the clinic buyer, it is the difference between "we charge €X" and "we collected €Y, here's the GAAP-aligned reconciliation".
Audiences
- Investor: the existence of a portal that only sees money proves the HMS' tenancy model — a non-clinician can have full read on cents without any cleartext patient context. Same shell, hard boundary.
- Clinical buyer (clinic admin): this is the seat to give a part-time
accountant or an outsourced Steuerberater without granting access to charts.
Auth scope is
ACCOUNTANT_ONLY— noADMIN_PLUSslip-through. - Developer/partner: the simplest non-clinical config — no
needsBackendStatus, noextraElements, notaglineFormatter. Useful reference for new role portals that just need a sidebar + RBAC gate. - Internal (ops/support): dashboard data is stubbed (a TODO inside
dashboard/page.tsx:20-21notes billing endpoints are pending) — the shell ships ahead of the data wiring; track it through CR-31 / accounting rollout.
Architecture
Role config — the minimal form
ddx-web/src/lib/portal/role-configs.ts:29-39 is the canonical "simple
portal" config:
export const ACCOUNTANT_CONFIG: RolePortalConfig = {
roleKey: 'accountant',
authRequirement: 'ACCOUNTANT_ONLY',
roleBadge: { role: 'ACCOUNTANT', labelKey: 'roles.accountant',
icon: CalculatorIcon, variant: 'default' },
brandNameKey: 'portals.accountant',
};
No backend-status banner, no extra elements, no tagline formatter, no
fetchOrg: false override — the factory at
ddx-web/src/components/portal/createRoleLayout.tsx runs every default.
Layout — one-liner
ddx-web/src/app/[locale]/portal/accountant/layout.tsx:17 is two lines of
substance:
function AccountantNav() { return <PortalNavigation roleKey="accountant" />; }
export default createRoleLayout(ACCOUNTANT_CONFIG, AccountantNav);
The sidebar is fully driven by the nav config; nothing about it is hand-coded per page.
Navigation — five sections
ddx-web/src/lib/portal/nav-configs/accountant.ts:22-99 exports five
sections: Overview (dashboard), Billing (invoices/payments/billing),
Financial (revenue/expenses), Insights (reports), Other (messenger,
settings). All icons are Phosphor-SSR (@phosphor-icons/react/ssr —
accountant.ts:8-18).
Routes
portal/accountant/ ├── layout.tsx → ACCOUNTANT_ONLY shell ├── page.tsx → redirects to dashboard ├── dashboard/page.tsx → AccountantDashboardClient + RBAC gate ├── accounting/ │ ├── page.tsx │ ├── journal-entries/ → daybook entries │ └── reports/ → GAAP-aligned reports ├── billing/ │ ├── page.tsx → top-level billing summary │ └── invoices/ → invoice list/detail ├── messages/ + messenger/ → comms (shared infra; see messenger topic) ├── loading.tsx, error.tsx, not-found.tsx
The portal does not expose expenses/, payments/, or revenue/ as
standalone routes yet — the nav config references them, but the pages are
deferred. Adding them is a createRoleLayout-compatible drop-in (one
page.tsx + a translation key).
Tech Stack & Choices
| Layer | Choice | Why |
|---|---|---|
| Auth gate | requireRoleAccess('ACCOUNTANT_ONLY', locale) (dashboard/page.tsx:18) | Strict single role — never ACCOUNTANT_PLUS. Doctors must NOT see this portal accidentally. |
| Shell | Default createRoleLayout settings | The simplest possible role layout — proves the factory works without overrides. |
| Icon | CalculatorIcon (Phosphor-SSR) | One visual cue throughout the chrome. |
| Brand label | i18n key portals.accountant | Resolved via the default brand namespace (common). |
| Nav | Sectioned (5 sections via NavSection[] ) | Section grouping matches German accounting workflow (Buchhaltung separate from Abrechnung). |
| Data | Stubbed dashboard pending billing endpoints | dashboard/page.tsx:20-21 carries a TODO; the underlying billing API is the gate. |
Rejected: making accountant inherit from ADMIN_PLUS (would over-grant —
violates least privilege; an accountant must never see patient charts);
embedding accounting inside clinic-admin (would scatter the same KPIs
behind a role-detect flag, hurting the audit story).
Data Flow
- Accountant logs in → JWT has
role: ACCOUNTANT+organizationId(+ typically NOfhirPractitionerId). - Browser hits
acm.dudoxx.com/en/portal/accountant. proxy.tsstrips locale, resolves trigram →X-Clinic-ID: acme-clinic, confirms session.portal/accountant/page.tsxredirects todashboard.accountant/layout.tsxrunscreateRoleLayout(ACCOUNTANT_CONFIG, AccountantNav)— RBAC =ACCOUNTANT_ONLY, org is fetched, shell mounts.dashboard/page.tsxre-asserts RBAC, currently stubs financial data, and renders<AccountantDashboardClient locale userName>.- Future:
backendGet<BillingStatsResponse>('billing/stats')will populate pending invoices, payments, revenue — once those endpoints land (see [[billing]]).
Implicated Code
ddx-web/src/lib/portal/role-configs.ts:29-39—ACCOUNTANT_CONFIG(minimal form).ddx-web/src/lib/portal/role-configs.ts:188— registry entry.ddx-web/src/app/[locale]/portal/accountant/layout.tsx:13-17— layout + one-line nav delegate.ddx-web/src/app/[locale]/portal/accountant/page.tsx:7-12— root redirect todashboard.ddx-web/src/app/[locale]/portal/accountant/dashboard/page.tsx:14-29— RBAC-gated server page; rendersAccountantDashboardClient; lines 20-21 carry the open TODO for billing endpoints.ddx-web/src/lib/portal/nav-configs/accountant.ts:22-99— five-section sidebar config.ddx-web/src/components/portal/accountant/AccountantDashboardClient.tsx— client-side dashboard scaffold (placeholder until billing API lands).
Operational Notes
ACCOUNTANT_ONLYis strict — a SUPER_ADMIN cannot view this portal as the accountant; impersonation must use the super-admin tools, not URL walking. RBAC is asserted twice: in the layout (factory) and indashboard/page.tsx:18— both required so a child page error boundary cannot leak.- Dashboard is intentionally stubbed — the TODO at
dashboard/page.tsx:20-21is tracked: do not "fix" by hand-rolling ad-hoc fetches; the [[billing]] page documents the proper endpoint rollout. - Nav paths reference deferred routes —
payments,revenue,expensesexist as nav entries but not as pages. Clicking renders the 404 boundary. Either add the page or remove the entry; do not leave half-wired nav items. - Brand label namespace —
portals.accountantresolves under the default namespace; if you rename to a feature-specific namespace (e.g.accountant.brand.label) update both the config and all three locale JSON files (src/locales/{en,fr,de}/*.json) and runpnpm i18n:ci-check. - No
needsBackendStatus— the accountant doesn't see a degraded-API banner. Financial reads on a degraded backend will simply fail; consider enablingneedsBackendStatus: true, scope: 'children'when billing endpoints ship (mirroring receptionist's config).
Related Topics
- Multi-Portal Architecture — the shared factory and registry this portal participates in.
- Admin Portal — the operations counterpart; finance-adjacent clinic configuration lives there.
- Billing — backend endpoints that will feed this dashboard.
- Accounting — chart-of-accounts and
journal entries surfaced through
accounting/routes. - i18n — Multi-Lingual Interface — translation namespaces consumed by the accountant nav config.