The problem this solves
The chart of accounts is the vocabulary the entire finance function speaks. Get its structure right and everything downstream — planning, consolidation, reconciliation, allocation — has something coherent to attach to. Get it wrong and every EPM implementation that follows spends its budget on workarounds. The accounting flexfield here is six segments: Company-CostCenter-Account-Product-Intercompany-Future, and only two of them mean anything to EPM.
That grain difference is the whole reason a mapping layer exists, and it is where the quiet failures live. A combination the GL rejects is loud — someone sees an error and fixes it. A combination the GL happily accepts but EPM has no mapping for is silent: either the row never arrives, or it lands somewhere that looks plausible. Nobody notices until a balance does not tie three months later.
Audience
ERP and EPM architects designing or reviewing a chart of accounts. Integration developers building the ERP→EPM data flow. Controllers who need to know why a new cost centre did not show up in their EPM report.
Six segments, two of which EPM cares about
| Segment | Role | Maps to |
|---|---|---|
| NLQ query: “Show the chart of accounts for NYC Flagship” or “Cross-validation rules for the cash account” — DeepSeek resolves the company and the natural account | ||
| Company (3) | Balancing segment — every journal must balance within it | EPM Entity |
| Cost Center (4) | Where the cost lands | Summarised away |
| Account (5) | Natural account — determines P&L or balance sheet | EPM Account |
| Product (4) | Merchandise family | Summarised away |
| Intercompany (3) | Counterparty company — drives FCCS elimination pairing | Summarised away |
| Future (3) | Reserved, always 000 — every ERP has one | Summarised away |
44 companies, 27 natural accounts, 12 cost centres, 5 product families, and eight cross-validation rules governing which combinations of them the ledger will accept.
A live combination, validated and mapped
- The code combination rendered segment by segment as you change any selector
- Validation verdict — accepted, or rejected with every rule that fired and why it exists
- ERP → EPM mapping panel showing which two segments resolve to a dimension, which four are summarised away, and any mapping issue split into fatal (never reaches EPM) versus silent (lands, but loses grain)
- Seven presets that break specific rules or trigger the unmapped cases on demand
Upstream of everything else in the lab
This is the structure the rest of the portal depends on. Journals & Balancing (UC17) posts to these combinations — and every line it generates is asserted to pass these rules. Trial Balance & Close (UC18) aggregates them and proves the result ties to the FP&A income statement. The EPM modules then plan, consolidate, reconcile and allocate what this chart of accounts made it possible to record.
Recommended integration points
- COA design workshops: the segment table and the two-of-six mapping reality are the fastest way to explain why EPM cannot simply mirror the GL
- Integration build: the fatal-versus-silent distinction is the specification for how the load should behave on an unmapped value
- New value onboarding: the process gap this demo dramatises — a value added in the ERP without a corresponding EPM mapping — is the single most common cause of a load that reconciles yesterday and not today
The numbers
The honest checklist
- โYou are designing or reviewing a chart of accounts that an EPM application will consume
- โYour ERP→EPM load has ever produced a number that did not tie, and nobody could say which segment value caused it
- โYou need to explain to a non-technical audience why EPM is at a different grain from the GL
- โYour organisation has a single-segment or two-segment chart of accounts — there is no mapping problem to illustrate
- โYou need the actual cross-validation rules from your own instance modelled — these eight are representative, not extracted from a client system
Try it now
The live demo validates and maps any combination of the six segments in real time.
Things to try
- Press “Store posting to Corporate Legal” — CVR-04 fires, because cost centre 8xxx belongs to a 9xx company. Then press “Revenue with no product” and watch a different rule fire for a different reason
- Press “Unmapped company” and then “Unmapped cost centre” back to back — the first is fatal and the row never reaches EPM; the second still loads and only loses grain. Two very different conversations with the client
- Type “Cross-validation rules for the cash account” and note the page repairs the rest of the combination so a balance-sheet account is not shown breaking a rule it did not cause
Two kinds of failure, and only one of them is loud
A cross-validation failure is a good failure. The GL refuses the posting, someone sees an error, and the combination gets corrected at source. The cost of the control is a moment of friction at data entry, and the benefit is a ledger whose combinations mean what the design says they mean.
A mapping failure is the dangerous one, and it has two forms that clients routinely conflate. If the company has no EPM Entity mapping, the row is rejected by the load: the amount is missing from EPM entirely, and a total that used to tie no longer does. If the cost centre has no mapping, nothing visibly breaks — the cost centre is summarised away before EPM sees it, so the amount still lands on the right entity and account. What is lost is any downstream rule keyed on that cost centre. The first is a reconciliation break; the second is a silent behaviour change. Treating them as the same problem produces either panic or complacency, and this demo separates them deliberately.
Selectors → combination → two independent verdicts
- Resolves the natural account object so downstream rules can read its type and flags
- Company code comes from
companyCode(entityId)— blocked by entity type, unique by construction
- Eight pure predicates over the combination
- Reports every failure, not the first — a combination can break two rules at once
- Company → Entity, Account → Account
- Four segments summarised away
- Issues split fatal vs silent
- Segment-by-segment code, verdict, every violated rule with its rationale
- Mapping panel with per-segment targets and any issue, plus the flexfield and rule reference tables
App UI โ component breakdown
| Component | Behaviour |
|---|---|
| Segment selectors | Company (44), cost centre (all 12, deliberately — invalid combinations must be reachable), account (27), product (5), intercompany (44 + none) |
| Code combination | Rendered segment by segment, monospaced, updating on every change |
| Verdict + violations | Accepted, or rejected with each failing rule’s id, label and the reason the rule exists |
| Mapping panel | Per-segment: mapped dimension in green, or “summarised away” in grey; issues rendered red (fatal) or amber (silent) |
| Presets | One valid, four rule-breakers, two mapping failures — each is a teaching moment rather than a random invalid combination |
| Reference tables | The flexfield and all eight rules, always visible below the interactive area |
Six segments, and why each one exists
The Company segment is not just another attribute — it is the balancing segment, and that single property drives the behaviour in UC17 that surprises people most. Account is the natural account, and its leading digit determines whether the line is balance sheet or P&L, which in turn drives half the cross-validation rules. Those two carry accounting meaning.
Cost Center and Product are management-reporting dimensions: they make the P&L useful to operators without changing what the ledger asserts. Intercompany exists purely to make elimination possible downstream — it records the counterparty so FCCS can pair the two sides. And Future is reserved, always 000. Every ERP has one, every implementation swears it will be used within two years, and CVR-08 exists to make sure nobody starts using it before it has been formally opened.
// Natural accounts reuse the numeric part of the FP&A account ids exactly:
// GL 40001 โ EPM A_40001 (Store Revenue)
// GL 60004 โ EPM A_60004 (Technology & G&A)
//
// Not a coincidence โ it is the point. The GL and the EPM Account dimension
// should be the same numbers, and a demo that used different ones would be
// teaching a mapping problem that good design avoids.
Eight predicates the ledger enforces
| Rule | Enforces | Why |
|---|---|---|
| CVR-01 / 02 | Balance sheet accounts carry no cost centre and no product | The balance sheet is entity-level. A cost centre on a cash account implies a departmental cash balance, which does not exist |
| CVR-03 | P&L accounts require a real cost centre | Every cost belongs to a department, or management reporting has a hole in it |
| CVR-04 | Corporate cost centres belong to corporate companies | A store cannot post to Corporate Legal. This is the rule that catches genuine miskeying most often |
| CVR-05 | Revenue and merchandise cost require a product | Margin is reported by product family; a revenue line with no product is invisible to that report |
| CVR-06 | Intercompany accounts require an IC segment, and no other account may carry one | Both directions matter — a missing IC value breaks elimination, a spurious one creates a phantom pair |
| CVR-07 | The IC segment cannot equal the company | An entity cannot trade with itself |
| CVR-08 | The future segment stays 000 | Reserved until formally opened |
Each is a one-line pure predicate over the combination, which makes them individually testable — the suite builds a combination that violates each rule in turn and asserts that rule fires. It also means the UI can report all failures rather than short-circuiting on the first, which matters because a genuinely wrong combination usually breaks two rules at once and fixing only the reported one just produces a second error.
Two of six, and two ways to fail
mapToEPM(combination) โ {
epmEntity, // from Company โ null if unmapped
epmAccount, // from Account โ null if unmapped
summarised: ['Cost Center', 'Product', 'Intercompany', 'Future'],
issues: [{ seg, value, effect, fatal }],
loads: issues.every(i => !i.fatal)
}
The fatal flag is the whole design. A missing company mapping is fatal: the row is rejected and the amount never reaches EPM, so a total that reconciled last month does not this month. A missing account mapping is equally fatal, and worse in character — in many real integrations the row does not fail but posts to a default member, silently inflating it. A missing cost centre mapping is not fatal at all, because the cost centre is summarised away before EPM sees it; the amount lands correctly and only a cost-centre-keyed mapping rule would miss it.
The demo plants both non-fatal and fatal cases so the difference can be shown rather than described: cost centre 1200 (Visual & Marketing) was opened in the GL after the mapping was last published, and LAD Digital was stood up as a company without ever being added to the EPM Entity dimension. The unmapped-company constant is derived from the entity list rather than written as a literal, so renumbering company codes cannot leave a stale value behind — a small thing, but exactly the kind of drift this use case is about.
Three fields, and the account is a five-digit enum
The COA schema is {intent, entity, account, confidence}. Entity resolves the company segment through the same ENTITY_ALIASES table the other entity-bearing use cases share; account is a 27-value enum of natural accounts, matched either as a literal five-digit code or through an alias table (“cash” → 11001, “cogs” → 50001, “g&a” → 60004). Nothing about a value reaches the model — only structure. The interesting design decision is on the UI side: when NLQ resolves an account, the page repairs the rest of the combination so it stays legal (a balance-sheet account forces cost centre and product to 0000), because a user asking about the cash account wants to see the cash account, not a cross-validation error they did not create.
Cost model — cost per query
| Component | Detail | Cost |
|---|---|---|
| LLM input tokens | ~700 tokens (schema block + few-shot examples + query) | $0.000098 |
| LLM output tokens | ~45 tokens | $0.0000126 |
| Combination validation + mapping | Client-side, deterministic, once the query resolves | $0 |
| Total per query | ~$0.0001 | |
| With 2× safety margin | ~$0.0002 |
Slightly larger than the smallest schemas because the natural-account enum and its aliases have to be grounded. Validation and mapping are pure client-side predicate evaluation once the query resolves.
Key files
| File | Role |
|---|---|
epm-nlq-src/assets/epm-erp-gl-data.js | COA_SEGMENTS, NATURAL_ACCOUNTS, COST_CENTERS, CVR_RULES, companyCode(), validateCombination(), mapToEPM() |
epm-nlq-src/pages/chartofaccounts.html | UI: NLQ bar, five segment selectors, the live code combination, validation verdict with every rule that fired, the ERP→EPM mapping panel, and seven presets that break specific rules on demand |
epm-nlq-src/assets/epm-data.js | Shared ENTITIES, PERIODS and genIS() — the P&L amounts the journals post, and therefore what the trial balance reproduces |
epm-nlq-src/assets/epm-fccs-data.js | genConsolidationBS() — the balance sheet side, shared with the FCCS and ARCS modules |
functions/api/nlq-query.js | Shared NLQ endpoint; this use case uses useCase: "coa" |
Why the ledger is generated from genIS(), not beside it
The tempting shortcut is to generate plausible journals independently and let the trial balance be roughly right. That would have broken the one claim this module exists to make. Instead every journal line amount is driven by genIS(), so the posted trial balance reproduces the FP&A income statement to the dollar — asserted by unit test across multiple entities and periods. It is the same discipline the FCCS module applies when it constructs a local balance sheet that balances before translation: if the synthetic data does not tie, the demo teaches the wrong lesson.
Tech stack — every tool in this build
| Layer | Tool | Why |
|---|---|---|
| Data layer | epm-erp-gl-data.js (vanilla JS) | Deterministic predicate evaluation — each cross-validation rule is a one-line pure function |
| LLM | DeepSeek V3 | Shared NLQ endpoint; a three-field schema with a 27-value account enum and an alias table |
| Charts | None — this use case is structural, and a chart would add nothing | Shared library with the other kits |
| 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 |
|---|---|
| A combination that violates policy gets posted | Every rule in CVR_RULES is a pure predicate evaluated on every render; the UI reports all failures rather than the first, so a combination cannot look fixed while still breaking a second rule |
| An unmapped company silently loads to a default EPM member | mapToEPM() marks a missing company mapping fatal — the row is reported as rejected, never defaulted. The demo plants exactly this case (LAD Digital, stood up in the GL after the EPM Entity dimension was last published) |
| An unmapped cost centre is assumed to be equally fatal | It is not, and treating it as such teaches the wrong lesson. The cost centre is summarised away, so the amount still lands — the loss is any mapping rule keyed on it. Flagged as non-fatal and worded to say so |
| Segment values drift between the ERP and the mapping | The unmapped-company constant is derived from the entity list rather than hardcoded, so renumbering company codes cannot leave a stale literal behind |
Guardrails — what prevents bad ledger math
- Company codes are provably unique: assigned by position within type-blocks and unit-tested for uniqueness across all 44 entities — a collision would make the balancing segment ambiguous, which is the worst possible bug in a general ledger
- Every rule is independently violable: the test suite constructs a combination that breaks each of the eight rules in turn and asserts that rule — and only that rule — fires
- Cross-validation and mapping are separate concerns: a combination can be perfectly legal in the GL and still fail to reach EPM. The UI shows them in two panels for exactly that reason
- Every journal line in UC17 passes cross-validation: asserted in the test suite, so the generated ledger could not itself have posted an illegal combination
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 ERP Cloud (Fusion) 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
- Fusion 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: job roles (General Accountant, General Accounting Manager) composed of duty roles, plus data roles
- Data level: Value set security on segment values, plus data access sets — a user may be entitled to the structure without being entitled to every company code within it
- 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 ERP Cloud (Fusion) does not replace your directory — it trusts it. Fusion 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 ERP roles through SCIM provisioning. |
| OCI IAM (identity domains) | Native | Already present with Oracle ERP Cloud (Fusion). 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 ERP 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 โ ERP role / entitlement
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FIN-EPM-Analysts โ General Accountant (inquiry)
FIN-EPM-Controllers-EMEA โ General Accounting Manager + EMEA data scope
FIN-EPM-Admins โ Financial Application Admin
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
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: A Fusion inquiry job role such as General Accountant; maintaining segment values is a separate, narrower role this tool never needs.
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 ERP API; calls are made as the user | A single read-only integration account calls the API; the application filters the results |
| Enforcement point | Oracle ERP Cloud (Fusion) | 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: Value set security on segment values, plus data access sets — a user may be entitled to the structure without being entitled to every company code within it.
- 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 Fusion security console, 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 Chart of Accounts & Segment Mapping
| Concern | Answer for this use case |
|---|---|
| Native entitlement required | A Fusion inquiry job role such as General Accountant; maintaining segment values is a separate, narrower role this tool never needs |
| Data-level control | Value set security on segment values, plus data access sets — a user may be entitled to the structure without being entitled to every company code within it |
| Use-case-specific sensitivity | The chart of accounts exposes legal-entity structure, cost-centre organisation and product families on one screen. Classify it, scope which segments a group may browse, and remember that a full COA export is a competitive-intelligence document. |
9.8 · Security configuration checklist
- ✓Oracle ERP Cloud (Fusion) 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 Chart of Accounts & Segment Mapping 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 ERP responsibilities and data-access sets to the request
- Rate limits per user, blocks anonymous access, scrubs PII patterns before anything reaches the orchestrator
- L2 grounding reads the live chart of accounts and segment value sets on a schedule — not a hardcoded schema
- Only the schema + user query go to the model; ledger amounts never leave the data layer
- L4 rejects any segment value outside the approved value sets and falls back to the deterministic parser
- Data stays inside the OCI boundary
- Natural fit when Fusion ERP is already in OCI
- Use the hyperscaler the org already governs
- Enterprise DPA, no training on prompts
- For regulated or sovereign ledgers
- Highest control, highest run cost
- Oracle ERP Cloud — the chart of accounts structure, segment value sets and cross-validation rules via the ERP REST API (or a scheduled BI Publisher extract), refreshed with change detection
- Least-privilege read-only integration user, scoped to the required ledgers and data-access sets, credential in a vault and rotated
- Results filtered to the requesting user’s own ledger and data-access-set security before rendering
- Every query logged: user, timestamp, raw query, parsed intent JSON, model + prompt version, ledger/period 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 — the same line the demo prints today
- Every number on screen drills to a journal line an auditor can reproduce in the ERP
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) | Fusion ERP 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 / Google Vertex AI | 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; the ledger’s data classification forbids 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. |
| Integration user | One read-only ERP integration user per application per environment, scoped to the required ledgers, 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 the ERP 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 segment value against the approved value sets and strips unexpected keys; production adds a check that the resolved ledger and period are inside the user’s data-access set before the query runs. |
| Data minimisation | Prompts contain metadata and the user’s query only. No journal amounts, no supplier or customer names, no journal descriptions. 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 window onto the general ledger does not change a financial-reporting control, but the GL is the most SOX-relevant system in the estate and this 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, ledger/period/account 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 segment value sets, 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 integration-user owner is not a developer; production secrets are held by platform operations. Critically, this tool grants no posting ability — it cannot create, approve or post a journal. |
| Access recertification | Quarterly review of who can use the tool and of the integration user’s ERP roles and ledger access, 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 drills to a journal line, a batch and a source; an auditor can re-run the same query in the ERP 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 | ERP target | Data | Gate to leave |
|---|---|---|---|
| Dev | ERP Dev instance | Synthetic or masked | Unit tests on the data layer; lint; eval suite ≥ threshold against the Test LLM deployment |
| Test / UAT | ERP Test instance (post-refresh) | Masked copy of production | Business UAT sign-off on the golden queries; security scan; performance run (p95 latency, fallback rate) |
| Prod | ERP Production | Live | Change ticket approved; deploy in window; smoke test; hypercare with rollback ready |
- Promoted artifacts: application build, prompt templates (versioned), approved segment-value 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.
- Chart of accounts drift: a scheduled job refreshes segment value sets and the cross-validation rule list into Layer 2 grounding with change detection, so a new cost centre or company appears in the approved lists without a code release — and an unmapped one is surfaced rather than silently accepted.
- 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.
- Close-window load: GL queries spike hard in the first five working days. Size for the close, not the average, and cache the chart of accounts aggressively — it barely changes.
- 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; ERP API outage → cached metadata with a stale banner; guardrail spike → review logs for injection attempts.
10.7 · What changes for Chart of Accounts & Segment Mapping
| Concern | Production answer |
|---|---|
| System of record | Oracle ERP Cloud — the chart of accounts structure, segment value sets and cross-validation rules via the ERP REST API (or a scheduled BI Publisher extract), refreshed with change detection |
| Read/write posture | Read-only. Segment values and cross-validation rules are maintained in the ERP under change control; this layer explains them. |
| Use-case-specific control | The chart of accounts is itself sensitive — it exposes legal-entity structure, cost-centre organisation and product families. Classify it and scope who may browse which segments; a full COA export is a competitive-intelligence document. |
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 ERP roles, ledgers and data-access sets; cookie gate removed
- ✓Read-only ERP integration user per environment, credential in a vault, rotation scheduled, no posting privilege
- ✓Prompts, segment value sets, 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
- ✓Chart-of-accounts refresh job scheduled with change detection for new and unmapped values
- ✓SLOs sized for the close window, cost caps, and the incident runbook agreed with platform operations