Apex Flow Technology · Validation Evidence

Oracle Opera / OHIP Folio Ingest — Validation Results

The two billing edge cases that break automated corporate invoicing in a multi-brand, multi-currency estate: split-folio routing and cross-border corrections. Both are covered by the mapper and asserted on every build.

MEASURED against synthetic payloads in our own test harness — 17/17 mapper assertions passing, plus 11/11 smoke-test checks in dry-run. These are not yet results from a live Opera instance; the same suite runs unchanged against a customer OHIP sandbox once read-only credentials are issued, which is the first deliverable of a 60-day shadow pilot.

Scenario A · Routing & split folios

A personal bar tab must never land on the corporate invoice

  • Only the company window is consolidated onto the corporate invoice
  • Personal incidentals on windows 2+ never reach the corporate invoice
  • An explicit routing instruction overrides the window number in both directions
  • Reversed postings are dropped so credits are never invoiced
  • Single-window folios (no window attribute) behave unchanged — no regression
  • Corporate windows are configurable per property (Opera window config varies)
  • Every retained line keeps a stable OPERA_-prefixed charge id — no cross-source collisions
Scenario B · Foreign currency corrections

A THB posting on a EUR contract converts once, at Opera's rate — or not at all

  • Property-currency lines convert to the contracted settlement currency using Opera's own exchange block
  • Converted amounts match rate x gross to 2dp with no rounding drift
  • Lines already in the contracted currency pass through untouched
  • A line with no usable rate keeps its native currency — no invented rate, reconciliation flags it
  • Every posting carries an explicit currency code
Live sandbox smoke test

What runs on day one of a shadow pilot

  • OAuth2 token exchange against the OHIP host
  • Single read-only folio GET — no POST, PATCH or DELETE anywhere in the script
  • Folio windows flattened with window number, routing instruction and reversal flags preserved
  • The same Scenario A and B assertions re-run against whatever the live sandbox returned
  • Timestamped JSON report written as the pilot connection-test artefact
  • Non-zero exit on any failed check

Required from your side: an OHIP sandbox host, read-only client credentials, an application key, and a test reservation — ideally one with a genuine split folio and a cross-currency posting. Nothing is written back to Opera at any stage.

Configuration note

Transaction-code ranges and folio window conventions are property-configurable in Opera. Our defaults are confirmed against your actual OHIP configuration during pilot setup — a configuration exercise, not a code change.

Appendix A · Technical security

Stateless processing — guest data never reaches storage in readable form

Stateless edge workers

Ingest and aggregation run in Cloudflare Workers / Deno isolates with no local disk, no session state and no cross-request caches. Each folio is processed inside a single request lifecycle; the isolate holds nothing once the response is returned.

Guest PII processed in-memory only

Guest name, room number and folio narrative exist only as local variables during aggregation. They are never written to a table, a log line or an object store. References are released before the handler returns so the runtime GC reclaims them immediately.

Hashed identifiers at the persistence boundary

What is persisted is the corporate-billing skeleton: a hashed reservation key, PMS charge id, amounts, currency, tax and item type. Guest identity is replaced by a salted SHA-256 digest, so line items reconcile across runs without any table holding a readable guest name.

OAuth2 credentials held server-side

OHIP client id, secret, integration user and application key are read from environment secrets inside the server handler only. They are never present in a client bundle, never returned to the browser, and bearer tokens are cached per-isolate in memory with a 60-second pre-expiry refresh.

Read-only by construction

The OHIP client issues GET requests only. There is no POST, PATCH or DELETE path to Opera anywhere in the ingest codebase — write-back is absent from the implementation, not merely disabled by configuration.

Failure isolation

The validation layer is an observer. A fault, timeout or deploy inside it cannot alter a folio, block a checkout or affect the PMS ledger, which remains the sole source of truth for the pilot.

Database isolation boundary

Every tenant-scoped table enforces PostgreSQL Row Level Security keyed on organisation membership, with least-privilege grants issued per table. An automated validation report blocks release on any table lacking isolation controls.

Auditability

Every ingest carries a trace id linking request, mapping decision and resulting line items, retained for the full validation window and exportable for your security review.

Appendix B · OHIP data mapping

Every OHIP field we request — and nothing else

OHIP fieldWhy it is requiredPersisted
reservationIdCorrelate folio lines to one stayHashed (salted SHA-256)
hotelIdProperty attribution and tax jurisdictionYes — plain code
folioNumberGroups postings under one folioHashed (salted SHA-256)
businessDateInvoice period allocationYes — date only
transactionLines[].idIdempotency key; prevents double-billingYes — OPERA_-prefixed
transactionLines[].transactionCodeClassifies room / incidental / taxYes
transactionLines[].descriptionInvoice line narrativeYes — charge text only
amount.grossAmountInvoice line grossYes
amount.netAmountNet for tax reconciliationYes
amount.taxAmountVAT / regional tax complianceYes
amount.currencyCodeNative posting currencyYes
amount.exchange.*Single conversion to contracted currencyYes — rate + target code
folioWindowExcludes personal windows from the corporate invoiceYes — integer
routingInstructionExplicit company/guest override of windowYes — enum
isReversedDrops reversed postings so credits are never invoicedNot stored — filter only

15 fields, all within the folio transaction block. Read scope only.

Explicitly not requested, not received, not stored

  • Guest name, email, phone or address
  • Loyalty number or profile id
  • Card tokens, PAN fragments or payment instruments
  • Passport, ID document or nationality fields
  • Rate codes, negotiated rates or commercial terms beyond the posted amount
  • Any reservation field outside the folio transaction block