The problem this solves
A global retailer's subsidiaries keep their books in local currency โ a Tokyo store's ledger is in yen, a Sรฃo Paulo store's is in reais. The consolidated group reports in one currency, usually the parent's. Getting from 17 sets of local-currency books to one USD-denominated consolidated statement is currency translation, and it is one of the two or three things a consolidation system exists specifically to do โ a spreadsheet cannot do it correctly at scale because the mechanics require three different rates applied to three different classes of account, and the residual has to plug to a specific equity line, not vanish.
Get translation wrong and the damage is not cosmetic. A misapplied rate on a $30M balance sheet line moves consolidated equity by real money, and auditors test this specifically at year-end.
Audience
Corporate consolidation accountants and controllers who own the group's translation policy. FP&A and treasury teams who need to understand why FX movement shows up in equity (CTA) rather than in the income statement. EPM architects scoping an FCCS or consolidation implementation.
A store and a period
Pick any of the 44 entities in the cube โ 27 of them report in a currency other than USD. The demo builds a balanced local-currency balance sheet for that entity (from its existing revenue seed in the FP&A module) and translates it using that currency's average, closing, and historical rates for the selected year and scenario.
| Input | Drives |
|---|---|
| Entity | Functional currency and the local-currency balance sheet magnitude |
| Year / Scenario | The revenue base the local-currency BS is built from (shared with the FP&A module) |
A rate-tagged translation table and a CTA waterfall
- Translation table: every BS line shown local-currency vs USD, with a colour-coded pill naming which rate translated it โ Closing, Average, or Historical
- CTA line: shown as a plug, not an input โ computed as whatever balances the translated statement
- CTA chart: every non-USD entity ranked by CTA, so a strengthening-EUR store and a devaluing-ARS store sit at opposite ends of the same chart
Between local books and the consolidated statement
Translation runs after each entity's local-currency trial balance is finalized and before intercompany eliminations and ownership adjustments are applied โ this demo's sibling use cases, IC Eliminations and Ownership & NCI, both assume translated (USD) numbers as their starting point in a real consolidation.
Recommended integration points
- Month-end close: Treasury publishes the period's average and closing rates; consolidation re-translates every non-USD entity in one batch run
- FX exposure review: CFO asks "how much of this quarter's equity movement is FX, not operating performance?" โ the CTA chart answers it directly
- Board reporting: explaining why EBITDA looked flat in local currency but consolidated USD revenue dropped โ a rate story, not an operating story
The numbers
| Account class | Rate |
|---|---|
| Assets & Liabilities (Balance Sheet) | Closing rate โ period-end spot |
| Common Stock, Opening Retained Earnings | Historical rate โ rate when equity was contributed |
| Net Income (current period) | Average rate โ period average |
| Cumulative Translation Adjustment | Plug โ not translated, derived |
The honest checklist
- โYou consolidate entities that keep their books in a currency other than the group reporting currency
- โAuditors or the board ask how much of equity movement is FX rather than operating performance
- โYou need average, closing, and historical rates applied to the correct account class automatically, not by spreadsheet formula
- โEvery entity already reports in the group currency โ there is nothing to translate
- โYou need remeasurement (functional currency โ local currency, gain/loss through the income statement) rather than translation (functional = local, adjustment through equity) โ that is a different mechanic with a different rate table
Try it now
The live demo translates any of the 44 entities in real time, driven by NLQ or the filter dropdowns.
Things to try
- Type "Show CTA for Buenos Aires" into the NLQ bar and watch CTA turn sharply negative from the currency's devaluation between historical and closing rates
- Select NYC Flagship (USD) โ confirm CTA is exactly $0, since a USD entity has nothing to translate
- Compare Berlin Mitte (EUR, appreciating) against Tokyo Shibuya (JPY, depreciating) on the CTA chart
Why three rates, not one
A single blended rate would be simpler to build and wrong. Assets and liabilities are settled at today's exchange rate, so they translate at the closing rate. Equity was contributed at a point in the past and stays fixed in translated terms unless the entity issues more stock, so it translates at the historical rate from that contribution date. Income is earned continuously through the period, so a period-average rate is the standard approximation for the income statement's translated value. Mixing these up โ translating equity at the closing rate, say โ produces a balance sheet that doesn't tie and an equity movement that has no accounting explanation.
Local BS โ rate-tagged translation โ CTA plug
- Builds a LOCAL-currency BS from the entity's revenue seed (shared w/ FP&A)
- Constructed to balance exactly: Assets = Liabilities + Equity
- Assets/Liabilities × closing rate
- Common Stock/Opening RE × historical rate
- Net Income × average rate
- CTA = Assets(USD) − Liabilities(USD) − Equity(USD) before CTA
- Rate-tagged table
- Chart.js horizontal bar of CTA across entities
The local-currency BS is deliberately constructed to balance exactly before translation (genConsolidationBS derives liabilities as a plug against assets and equity, not as an independent fraction). That discipline matters: if the local statement didn't tie, the CTA plug after translation would conflate a real FX effect with a synthetic-data bug, and the demo would teach the wrong lesson.
App UI โ Component breakdown
| Component | Behaviour |
|---|---|
| Rule callout | States the three-rate rule in plain language above the table, with colour-coded rate pills matching the table below |
| Entity selector | All 44 entities, labeled with their functional currency |
| KPI row | Functional currency, closing rate, average rate, and current-period CTA โ colour-coded green/red by sign |
| Translation table | Every line: local amount, rate-type pill, USD amount โ the CTA row is visually marked as a "Plug", not a normal rate |
| CTA chart | Horizontal bar, every non-USD entity, sorted so the largest negative and largest positive CTA sit at opposite ends |
17 currencies, three rates each
FX_RATES defines average, closing, and historical rates for every currency the retail cube touches, expressed as USD per one unit of local currency (so usd_amount = local_amount * rate). Two design choices worth calling out:
- Pegged currencies (AED, SAR) carry identical average/closing/historical rates โ a UAE or Saudi entity has effectively zero CTA from FX, which is correct and a useful contrast against the free-floating currencies on the same chart.
- The historical rate is a simplification. A real FCCS application tracks the historical rate at the actual date each tranche of equity was contributed โ potentially many dates across a subsidiary's life. This demo uses one fixed historical rate per currency, which is the right level of fidelity for illustrating the mechanism without requiring a multi-year contribution history the rest of the cube doesn't model.
CTA is derived, never entered
// translateToUSD() โ the core of the mechanism
totalAssets_usd = (cash + ar + inventory + ppe) ร closingRate
totalLiabilities_usd = (ap + debt) ร closingRate
equityBeforeCTA_usd = commonStock ร historicalRate
+ openingRE ร historicalRate
+ netIncome ร averageRate
CTA = totalAssets_usd - totalLiabilities_usd - equityBeforeCTA_usd
totalEquity_usd = equityBeforeCTA_usd + CTA // now the statement balances
This is the entire mechanism. Because assets/liabilities move with the closing rate and equity mostly doesn't (except for the current period's income, which moves with the average rate), any exchange-rate movement between the historical rate and the closing rate shows up as a gap โ and that gap is CTA. It sits in equity, not in net income, which is precisely why FX translation gains and losses don't distort reported operating performance even when they're large.
Why Argentina looks different on the chart
The demo's Buenos Aires entity uses an ARS historical rate of 0.00160 against a closing rate of 0.00095 โ a roughly 40% devaluation baked into the illustrative rate table, deliberately steep so the CTA chart shows a real-world pattern: entities in historically inflationary economies (Argentina, and periodically several Latin American and a handful of other emerging markets) can produce CTA swings an order of magnitude larger than a Eurozone or UK entity in the same period.
In a real IFRS or US GAAP consolidation, once a country is classified as hyperinflationary (broadly, cumulative three-year inflation approaching or exceeding 100%), the accounting standard changes further โ IAS 29 requires restating the local financial statements for inflation before translation, using the reporting-date rate for everything rather than the average/closing/historical split modeled here. That is a materially different, additional step this demo does not implement; flagging that boundary explicitly is more useful than pretending the simple model covers it.
Cost per query
| Component | Detail | Cost |
|---|---|---|
| LLM input tokens | ~650 tokens (compact schema โ just entity/year/scenario, same shape as FP&A) | $0.000091 |
| LLM output tokens | ~60 tokens | $0.0000168 |
| Translation calculation | Client-side arithmetic once the query resolves โ no server round-trip | $0 |
| Chart render | Chart.js, client-side | $0 |
| Total per query | ~$0.00011 | |
| With 2ร safety margin | ~$0.0002 |
The LLM's job stops at resolving which entity, year, and scenario the query means โ the same three-field schema as the FP&A use case. Every dollar amount in the translation table and every bar in the CTA chart is client-side arithmetic against FX_RATES; the LLM never sees or computes a currency value. That split keeps the query cheap and keeps the financial math auditable independent of the LLM.
Cost model โ At-scale projections
The LLM cost scales with query volume the same way every other use case's does โ roughly $0.0002/query, so 1,000 translation queries a month runs about $0.20. The translation math itself runs entirely client-side against pre-seeded rates once the query resolves, so it does not add to that cost regardless of entity count or how many times a given entity is re-translated. The real-world cost driver that matters is operational, not compute: the treasury process that supplies and maintains the average/closing/historical rate table each period.
Key files
| File | Role |
|---|---|
epm-nlq-src/assets/epm-fccs-data.js | ENTITY_CURRENCY, FX_RATES, genConsolidationBS(), translateToUSD() |
epm-nlq-src/pages/translation.html | UI: entity/year/scenario filters, KPI row, rate-tagged translation table, CTA horizontal bar chart |
epm-nlq-src/assets/epm-data.js | Shared ENTITIES, ENTITY_SEEDS, and genIS() โ the local-currency BS is derived from the same revenue base the FP&A module already uses |
functions/api/nlq-query.js | Shared NLQ endpoint; Translation uses useCase: "translation" โ the same three-field entity/year/scenario shape as FP&A, reused rather than reinvented |
Tech stack โ Every tool in this build
| Layer | Tool | Why |
|---|---|---|
| Data layer | epm-fccs-data.js (vanilla JS) | Deterministic translation math โ the LLM only resolves entity/year/scenario |
| LLM | DeepSeek V3 | Shared NLQ endpoint, entity/year/scenario schema reused from FP&A |
| Charts | Chart.js 4.x | Shared with Analytics UC4 and Capital UC6; horizontal bar for the CTA ranking |
| Edge hosting | Cloudflare Pages | Static file, no server compute needed for this use case |
| Build | Eleventy v3.1.5 | Copies epm-nlq-src/pages/ and epm-nlq-src/assets/ to _site/ verbatim |
Known attack surfaces
| Threat | Mitigation in this build |
|---|---|
| Wrong rate applied to wrong account class | Rate selection is hardcoded per account class in translateToUSD(), not user-selectable per line โ eliminates the "picked the wrong rate" error class entirely |
| Stale FX rates | Demo uses a fixed illustrative rate table; production requires a defined refresh cadence tied to the close calendar, owned by treasury |
| Local statement doesn't balance pre-translation | genConsolidationBS() constructs liabilities as a plug against assets and equity specifically to guarantee this can't happen in the demo data |
| Hyperinflationary economies treated like any other | Documented explicitly in this kit as an unimplemented boundary (IAS 29 restatement) rather than silently producing a plausible-looking but non-compliant number |
Guardrails โ What prevents bad translations
- CTA is always derived, never an input: there is no code path that lets a rate error silently disappear โ it always surfaces as a CTA movement large enough to be noticed
- USD entities are a built-in sanity check: since USD/USD rates are all 1.0, any USD entity showing non-zero CTA would immediately indicate a bug in the translation function itself
- Balance construction: the local BS is guaranteed to balance before translation, isolating CTA to genuine currency effects
Who is asking, and what are they allowed to see?
The demo answers neither question — it has a cookie gate and no notion of a user. In production these are the two questions everything else rests on, and they have different answers: authentication is who you are, authorization is what you may see. Corporate SSO settles the first. Only Oracle EPM Cloud can settle the second, and the single most important rule in this section is that this application must never become the place where that decision is made.
9.1 · The identity chain, end to end
- MFA and Conditional Access are enforced here — device compliance, location, risk signals
- Returns an ID token (who the user is) and an access token (what they may call)
- Group membership arrives as a claim; the application never handles a password
- Verify signature, issuer, audience and expiry against the IdP’s published keys
- Read the group claims — there is no local user table and no local role table
- Short-lived access token with refresh-token rotation; session timeout set to the data classification
- The API is called as the user
- EPM enforces its own security natively
- Audit trail names the real user
- Preferred where the API supports it
- The application becomes the enforcement point
- Entitlements fetched separately, applied in one audited place
- Simpler and cacheable — and a filtering bug is a data breach
- Roles: Service Administrator, Power User, User, Viewer — assigned to groups, never to individuals
- Data level: FCCS data access by Entity and Scenario; the Entity hierarchy carries the same parent/child security as the consolidation itself
- Group → role mapping lives in the platform, not in this application
- The user sees exactly what they would see logging into the source system directly — no more
- Every query logged against the real end user, never a shared account
9.2 · Federating the corporate identity provider
Oracle EPM Cloud does not replace your directory — it trusts it. The EPM Cloud identity domain is federated with the corporate IdP so authentication happens where it already happens, under policies security has already written.
| Identity provider | Protocol | Notes |
|---|---|---|
| Microsoft Entra ID (formerly Azure AD) | SAML 2.0 or OIDC | The common case. Conditional Access, MFA and device compliance are enforced at Entra and inherited automatically. On-premises Active Directory federates through Entra Connect rather than being integrated directly. |
| Okta | SAML 2.0 or OIDC | Same pattern; Okta groups drive EPM roles through SCIM provisioning. |
| OCI IAM (identity domains) | Native | Already present with Oracle EPM Cloud. Can be the primary IdP for a smaller estate, or a federated spoke of Entra/Okta for a larger one. |
| AD FS | SAML 2.0 | Still seen where the estate is not yet cloud-first. Works, but you inherit the on-premises availability of the token service — if AD FS is down, nobody logs in. |
For the browser application itself, use OIDC Authorization Code flow with PKCE. Not the implicit flow, which is deprecated and leaks tokens through the URL, and never a resource-owner password grant — a finance tool should not be capable of handling a password at all.
9.3 · From group membership to EPM roles
Roles are granted to groups, never to individuals, and the groups come from the directory. That one discipline is what makes joiner/mover/leaver work without anyone having to remember this application exists.
Entra ID group โ EPM role / entitlement
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FIN-EPM-Analysts โ Planning User
FIN-EPM-Controllers-EMEA โ Power User + EMEA data scope
FIN-EPM-Admins โ Service Administrator
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Provisioned by SCIM. Remove the user from the group and the
entitlement disappears on the next sync โ including here.
For this use case the relevant native entitlement is: FCCS User with access to the consolidation application and the relevant scenario.
9.4 · The architectural decision: who enforces?
This is the choice that determines whether the deployment is defensible. Both patterns appear in the diagram above; the difference is where the security boundary actually sits.
| Pattern A — identity propagation | Pattern B — service account + filtering | |
|---|---|---|
| How | The user’s token is exchanged (OAuth 2.0 on-behalf-of) for one scoped to the EPM API; calls are made as the user | A single read-only integration account calls the API; the application filters the results |
| Enforcement point | Oracle EPM Cloud | This application |
| Audit trail shows | The real end user | The service account — you must log the real user separately |
| Failure mode | Token plumbing is more complex; per-user rate limits apply | A filtering bug is a data breach, and the entitlement copy drifts from reality |
| Verdict | Prefer this wherever the API supports user-token authentication | Acceptable with discipline: narrowest possible service account, filtering centralised in one tested place, real user in every log line |
The shortcut to refuse. Pattern B built with a Service Administrator account and no filtering at all is the most common way this gets delivered, because it works perfectly in UAT — testers are usually over-entitled, so nobody notices that everyone can see everything. It fails at the first access review, and by then it is in production with real users depending on it.
9.5 · Data-level security is the part that matters
Role membership decides whether a user can open the application. It does not decide which rows they get back, and confusing the two is the most expensive mistake available here.
- For this use case: FCCS data access by Entity and Scenario; the Entity hierarchy carries the same parent/child security as the consolidation itself.
- Apply it before aggregation, not after. Filtering a total that has already been computed across entities the user cannot see still leaks the total.
- The NLQ layer needs its own check. Layer 4 already validates that the resolved point of view uses approved members; production adds a second test — that the resolved POV sits inside this user’s scope — and it runs before the data call, not after. A natural-language interface is very good at asking for things politely; the authorization check must not care how the question was phrased.
- Fail closed. If entitlements cannot be resolved, return nothing and say so. An empty result is a support ticket; a permissive default is an incident.
9.6 · Provisioning, sessions and the leaver problem
- SCIM provisioning from Entra or Okta into the EPM Cloud identity domain (OCI IAM, formerly IDCS), covering joiner, mover and leaver. The mover is the case people forget — somebody changing region should lose the old scope, not accumulate both.
- No local user store. If this application keeps its own copy of who may do what, a leaver keeps access until somebody remembers to update it. Nobody ever does.
- Short-lived access tokens with refresh-token rotation; align session timeout with the data classification rather than with convenience.
- MFA and Conditional Access at the IdP — not reimplemented here. Device compliance and location policy come free with federation.
- Quarterly recertification of both the groups that grant access and the service account’s own entitlements, evidenced and signed.
- Break-glass access is a named, monitored, time-boxed account — never a shared credential in a password manager.
9.7 · What this means for Currency Translation
| Concern | Answer for this use case |
|---|---|
| Native entitlement required | FCCS User with access to the consolidation application and the relevant scenario |
| Data-level control | FCCS data access by Entity and Scenario; the Entity hierarchy carries the same parent/child security as the consolidation itself |
| Use-case-specific sensitivity | Treat the close calendar as an access control, not just a status. An unlocked period holds in-flight numbers that are not yet anybody’s to quote — surface the period lock state and the consolidation timestamp alongside every translated figure. |
9.8 · Security configuration checklist
- ✓Oracle EPM Cloud federated with the corporate IdP over SAML 2.0 or OIDC; the cookie gate removed entirely
- ✓Browser app uses OIDC Authorization Code + PKCE — no implicit flow, no password grant
- ✓MFA and Conditional Access enforced at the IdP, not reimplemented in the application
- ✓Roles granted to directory groups, never to individuals; SCIM covers joiner, mover and leaver
- ✓Enforcement pattern chosen deliberately — Pattern A where the API supports it, or Pattern B with filtering centralised and tested
- ✓Data-level security applied before aggregation, and the resolved POV checked against the user’s scope before the data call
- ✓No local user table and no local role table anywhere in the application
- ✓Every query logged against the real end user, even when a service account makes the call
- ✓Authorization failures fail closed and are logged as security events rather than swallowed
- ✓Quarterly recertification of access groups and of the service account’s own entitlements
From demo to a governed enterprise deployment
Everything above runs on synthetic data, a public LLM API key, a cookie gate, and no audit trail — deliberately, so the mechanics are inspectable. Taking Currency Translation to production is not a rewrite; the 4-layer pipeline and the data-layer contract survive intact. It is a controlled-change program across six workstreams: architecture, LLM platform, security, SOX/audit, environment promotion, and operations. This section is the checklist we run with clients.
10.1 · Production reference architecture
- Terminates SSO, validates the session, attaches the user’s EPM groups to the request
- Rate limits per user, blocks anonymous access, scrubs PII patterns before anything reaches the orchestrator
- L2 grounding reads dimension metadata from EPM on a schedule — not a hardcoded schema
- Only the schema + user query go to the model; financial values never leave the data layer
- L4 rejects anything outside the approved member lists and falls back to the deterministic parser
- Data stays inside the OCI boundary
- Natural fit when EPM is already in OCI
- Use the hyperscaler the org already governs
- Enterprise DPA, no training on prompts
- For regulated or sovereign data
- Highest control, highest run cost
- Oracle FCCS — translated balances and CTA from the consolidation engine via REST; FX rates from the FCCS rates cube (never a second rate table)
- Least-privilege service account (read-only role, one app, one pod) with the token in a vault and rotated
- Results filtered to the requesting user’s EPM security before rendering
- Every query logged: user, timestamp, raw query, parsed intent JSON, model + prompt version, POV returned, latency, cost
- Exported to the SIEM; retained per the SOX evidence schedule
- Dashboards for fallback rate, eval pass rate, guardrail hits, p95 latency, spend
- The parsed JSON is shown to the user as the explanation (“AI: entity · year · scenario — 93% confident”) — the same line the demo prints today
- Every number on screen traces to an EPM cell intersection an auditor can reproduce
10.2 · Choosing the LLM platform
The demo’s DeepSeek call is a placeholder for a single adapter, callLLM(system, user), behind Layer 3. Swapping the provider changes one function and zero business logic. Pick the platform the organisation already governs — the security and procurement review is the long pole, not the integration.
| Option | Choose when | Data posture |
|---|---|---|
| Oracle OCI Generative AI (Cohere Command, Llama) | EPM Cloud already lives in OCI; you want one cloud boundary and one contract | Prompts stay in the OCI tenancy; no training on customer data; dedicated AI clusters available for isolation |
| Azure OpenAI Service | Microsoft-first finance estate (Entra ID, Purview, Sentinel already in place) | Private endpoint, regional deployment, zero-retention by default under the enterprise agreement |
| AWS Bedrock (Claude, Titan) / Google Vertex AI (Gemini) | The org’s landing zone is AWS or GCP; VPC endpoints and IAM already audited | VPC/PSC private access, no data used for training, CloudTrail/Cloud Audit Logs integration |
| Direct enterprise API (Anthropic, OpenAI) | Fastest model access; acceptable when a zero-data-retention agreement and DPA are signed | ZDR endpoint, SSO-managed keys, SOC 2 report on file |
| Self-hosted open weights (Llama, Mistral, Qwen via vLLM) | Sovereign or air-gapped requirements; regulated data classification forbids any external inference | Full control; you own patching, eval, and capacity — budget for an MLOps owner |
Put a model gateway in front of whichever you choose (Azure API Management, OCI API Gateway, Kong AI Gateway, LiteLLM, or Portkey): it owns key custody, per-team spend caps, routing and fallback between models, prompt/response logging, and lets you retire a deprecated model without touching the application.
10.3 · Security controls
| Control | Implementation |
|---|---|
| Identity & access | Covered in full in section 09 — corporate SSO, group-to-role mapping, and the decision about who enforces data-level security. Listed here because it is a production gate, not because it is optional. |
| Service account | One read-only EPM service account per application per pod, least-privilege role, no interactive login, credential in a vault (OCI Vault, Azure Key Vault, HashiCorp Vault), rotated on a schedule and on staff change. |
| Secrets & config | No secrets in code or build artifacts; environment-specific config injected at deploy; .dev.vars-style files never leave a developer machine. |
| Network | Private endpoints to the LLM provider and to EPM where the platform supports them; egress allow-list so the orchestrator can reach exactly two hosts; TLS 1.2+ everywhere. |
| Prompt-injection & input guardrails | Layer 1 (already in the demo) blocks instruction-override patterns, enforces length and scope; extend with a classifier on the gateway and log every rejection. |
| Output guardrails | Layer 4 (already in the demo) validates every returned member against the approved lists and strips unexpected keys; production adds a policy check that the resolved POV is inside the user’s security scope before the data call. |
| Data minimisation | Prompts contain metadata and the user’s query only. No cell values, no employee names, no free-text comments from EPM. Logged prompts are classified and retained accordingly. |
| Encryption | In transit (TLS) and at rest (provider-managed KMS); audit logs on immutable storage with customer-managed keys where policy requires. |
10.4 · SOX, audit, and model-risk controls
A read-only NLQ layer does not change a financial-reporting control, but it is an interface to a SOX-relevant system and lands squarely in ITGC scope. Treat prompts, schemas, and eval sets as code — that single decision satisfies most of what an auditor will ask for.
| Requirement | How it is satisfied |
|---|---|
| Complete, immutable audit trail | Append-only log of user, timestamp, raw query, parsed JSON, model and prompt version hash, POV returned, and row count — WORM storage, retained for the evidence period (typically 7 years), exported to the SIEM. |
| Change management | Prompt templates, few-shot examples, approved-member schema, and code are version-controlled; every change follows ticket → peer review → test evidence → CAB approval → deploy. A prompt edit is a code change. |
| Segregation of duties | Developers cannot deploy to production; the service-account owner is not a developer; production secrets are held by platform operations. |
| Access recertification | Quarterly review of who can use the tool and of the service account’s EPM roles, evidenced and signed. |
| Testing evidence | A golden-query regression suite (the few-shot examples plus a larger labelled set) runs in CI before every release; pass rate and diffs are archived as release evidence. |
| Model risk management | An inventory entry (intended use, limitations, owner, validation date) in the model-risk register — the SR 11-7 pattern for financial services; periodic re-validation when the model or prompt changes. |
| Reproducibility & lineage | Every displayed number traces to an EPM POV and a consolidation/calculation timestamp; an auditor can re-query the same intersection in EPM and match it. |
| Explainability | The parsed intent JSON is the explanation and is shown to the user on every response — no hidden reasoning between the query and the data call. |
10.5 · Dev → Test → Prod promotion
| Environment | EPM target | Data | Gate to leave |
|---|---|---|---|
| Dev | EPM Test pod (developer slice) | Synthetic or masked | Unit tests on the data layer; lint; eval suite ≥ threshold against the Test LLM deployment |
| Test / UAT | EPM Test pod (full refresh) | Masked copy of production | Business UAT sign-off on the golden queries; security scan; performance run (p95 latency, fallback rate) |
| Prod | EPM Production pod | Live | Change ticket approved; deploy in window; smoke test; hypercare with rollback ready |
- Promoted artifacts: application build, prompt templates (versioned), approved-member schema snapshot, eval set, infrastructure config (IaC) — all from the same Git tag.
- Pipeline: branch → PR review → CI (tests + evals) → deploy to Test → UAT sign-off → CAB → deploy to Prod → smoke test. Hosting can stay on Cloudflare Pages/Workers or move to OCI Functions + API Gateway or the org’s standard platform — the code does not care.
- Configuration: per-environment secrets and endpoints injected at deploy; the same build runs in every environment.
- Metadata sync: a scheduled job refreshes dimension metadata into Layer 2 grounding with change detection, so a new entity or account appears in the approved lists without a code release.
- Rollback: previous build and previous prompt version retained; rollback is a redeploy, and because prompts are versioned it also reverts a prompt regression.
10.6 · Operating it
- SLOs: p95 latency, availability of the read path (the deterministic fallback keeps it alive when the LLM is down — already built), fallback rate as a quality signal, eval pass rate per release.
- Cost governance: per-user and per-team token budgets at the gateway; alert on anomalies; the unit-cost model earlier in this kit is the baseline.
- Model lifecycle: providers retire models on a schedule — re-run the eval suite on the successor before switching, and record the switch as a change.
- Incident runbook: LLM outage → fallback parser; EPM API outage → cached metadata with a stale banner; guardrail spike → review logs for injection attempts.
10.7 · What changes for Currency Translation
| Concern | Production answer |
|---|---|
| System of record | Oracle FCCS — translated balances and CTA from the consolidation engine via REST; FX rates from the FCCS rates cube (never a second rate table) |
| Read/write posture | Read-only. Displays FCCS-computed translation; does not translate independently. |
| Use-case-specific control | Respect the close calendar: show the period’s lock status and consolidation timestamp so users can tell a final translated number from an in-flight one. |
10.8 · Production readiness checklist
- ✓LLM platform selected from the governed list, DPA / zero-retention terms on file, gateway in front of it
- ✓SSO integrated; authorisation derived from EPM security groups; cookie gate removed
- ✓Read-only EPM service account per pod, credential in a vault, rotation scheduled
- ✓Prompts, schema, and eval set version-controlled and under change management
- ✓Append-only audit log wired to the SIEM with the agreed retention
- ✓Golden-query eval suite passing in CI; results archived as release evidence
- ✓Dev / Test / Prod pipeline with gates, IaC, and a rehearsed rollback
- ✓Model-risk register entry and owner named; first re-validation date set
- ✓Metadata refresh job scheduled with change detection
- ✓SLOs, cost caps, and the incident runbook agreed with platform operations