initiative delivery

OPERA Cloud Yield Market Lookup

Shiv Yadav Updated 2026-03-11 integrations pms-connectors
opera-cloud pms-connectors yield-market rmsyield monolith loyalty q1-2026

Initiative: OPERA Cloud Yield Market Lookup

Product: PMS Connectors Domain: Integrations JIRA: IC

Overview

Implement the OPERA Cloud RMSYIELD outbound integration — a synchronous REST API that OPERA Cloud calls during Look-to-Book (LTB) to retrieve a Yield Market Code and Guest Value for a given membership ID. This enables OPERA Cloud to filter yieldable rate codes and store customer value labels on reservations, powered by Duetto's loyalty segmentation (casino properties) or static configuration (non-casino properties).

Objectives

  1. Expose a synchronous REST API conforming to OPERA Cloud's RMSYIELD outbound specification (OAuth2 + Yield Market Lookup)
  2. Support casino properties via the existing loyalty tier bucket chain (membershipId → patron score → segment → yield market code)
  3. Support non-casino properties via a new static YieldMarketMapping configuration (membershipId → pattern match → yield market code)
  4. Reuse existing monolith infrastructure: JWT/KMS auth, SecurityFilterChain, MongoDB patterns
  5. Maintain a clean future path for DRE-accelerated lookups at casino properties

Linked Repositories

See repos.yaml for complete repository manifest.

Repository Role Notes
duetto primary OAuth2 endpoint, RMSYIELD controller, YieldMarketMapping, LoyaltyTierBucket fields
dari-haskell future DRE-accelerated path for casino properties (not v1)

Problem Statement

When a reservation agent performs a Look-to-Book in OPERA Cloud, the system needs to determine which yieldable rate codes to display based on the guest's loyalty tier or membership status. OPERA Cloud supports an outbound RMSYIELD integration that calls an external RMS to resolve a membership ID into a Yield Market Code (e.g., "HURDL") and Guest Value (e.g., "HIGH"). Duetto must expose this API.

The challenge: Duetto serves two fundamentally different property types through the same API contract:

Property Type Has DRE? Has Loyalty/Patron Scores? Resolution Strategy
Casino-resort (OPERA migrating to Cloud) Yes Yes — patron scores, tier buckets Computed: membershipId → score → tier → yield market code
Non-casino hotel (OPERA Cloud native) No Membership programs only Configured: membershipId → static mapping → yield market code

Architecture Decision

One API contract. One entry point. Two resolution strategies.

The endpoint lives on the Duetto Monolith (ui component) — the only system universally deployed for every hotel. Resolution logic branches based on whether the hotel has a Duetto loyalty program configured.

Why Monolith

  • Universally deployed — every Duetto hotel has the monolith; DRE is only deployed for casino/resort properties
  • Already handles inbound PMS calls — HTNG callbacks at /external/sync/ and /external/async/ run on the ui component today
  • ui component is stable — the 93-99% heap / 30-60s GC pauses are on appBatch servers; ui handles user-facing and integration traffic
  • Owns the data — loyalty programs, patron scores, tier buckets, hotel configuration all live in monolith MongoDB

Why Not DRE

  • Not deployed for non-casino OPERA Cloud hotels — would force DRE onboarding on properties that don't need it
  • Would require new remote callout back to monolith for loyalty resolution, adding latency and a failure point
  • DRE remains a future optimization for sub-10ms latency at casino properties

Why Not Frontdoor (Kestra)

  • Frontdoor's purpose is bringing external data INTO Duetto (SFTP, webhooks, email). RMSYIELD serves data OUT to OPERA Cloud — opposite direction.
  • No access to monolith's loyalty data without becoming a proxy
  • Kestra workflow engine overhead is inappropriate for sub-second agent-facing lookups

Why Not a New Microservice

  • The feature is 1 controller, 1 config collection, and 2 fields on an existing entity
  • A new service would either duplicate the loyalty chain or become a thin proxy to the monolith
  • Cannot justify a separate deployment pipeline, monitoring, and on-call rotation for a 3-4 person team

API Contract (OPERA Cloud Specification)

OAuth2 Token Endpoint

POST /oauth/v1/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}

Response:
{
  "access_token": "eyJ...",
  "expires_in": 3600,
  "token_type": "bearer"
}

Yield Market Lookup Endpoint

GET /par/v1/hotels/{HOTELID}/yieldMarketType?membershipId={MEMBERSHIPID}
Authorization: Bearer {token}

Response:
{
  "yieldMarketTypeInfo": {
    "membershipId": "OCIS0788D4",
    "yieldMarketType": "HURDL",
    "hotelId": "ROSIE",
    "flexValues": ["HIGH"]
  }
}

Monolith Changes Required

Change 1: YieldMarketMapping MongoDB Collection (non-casino hotels)

Per-hotel static mapping for properties without a Duetto loyalty program:

{
  "_id": { "hotel": "hotel_abc", "membershipType": "REWARDS" },
  "mappings": [
    { "membershipPattern": "*", "yieldMarketCode": "STANDARD", "guestValueLabel": "MEMBER" },
    { "membershipPattern": "VIP*", "yieldMarketCode": "HURDL", "guestValueLabel": "VIP" }
  ],
  "defaultYieldMarketCode": "TRANSIENT",
  "defaultGuestValueLabel": "NONE"
}

Follows the same MongoDB pattern as existing IntegrationConfiguration.

Change 2: New Fields on LoyaltyTierBucket (casino hotels)

Two new optional fields on the existing entity in data/src/main/java/com/duetto/model/loyalty/LoyaltyTierBucket.java:

  • String yieldMarketCode (nullable, MongoDB key "ymc")
  • String guestValueLabel (nullable, MongoDB key "gvl")

Same pattern as existing reinvestmentBasis field. Revenue managers configure these per tier bucket alongside existing segment definitions.

Change 3: RMSYIELD Lookup Controller

New controller at /par/v1/hotels/{hotelId}/yieldMarketType in frontend/src/main/java/com/duetto/frontend/app/controller/integration/:

  1. Validate OAuth2 bearer token
  2. Resolve hotelId → Hotel
  3. Branch on LoyaltyProgram presence:
  4. Casino path: LoyaltyScoreProvider → TierBucket match → read yieldMarketCode + guestValueLabel
  5. Non-casino path: YieldMarketMapping → pattern match → read mapping
  6. Format YieldMarketTypeInfo response

Change 4: OAuth2 Token Controller

New controller at /oauth/v1/token with OAuthClient MongoDB collection for per-hotel client credentials. Issues JWT using existing KMS signing infrastructure from ExternalApiAuthenticationFilter.

Change 5: SecurityFilterChain

New @Bean @Order(3) in SecurityConfiguration.java for /par/** and /oauth/** paths using existing BearerTokenValidator pattern.

What Does NOT Change

  • DRE (dari-haskell) — no changes for v1
  • Frontdoor (Kestra) — not involved
  • Integrations service — not involved (being decommissioned)
  • Existing loyalty flow — unchanged; RMSYIELD reads the same data the loyalty UI already uses

Effort Estimate

Work Item Days Notes
YieldMarketMapping collection + CRUD 2 New MongoDB collection, admin API
LoyaltyTierBucket fields 1 Two optional fields on existing entity
OAuth2 token endpoint 2 Leverages existing JWT/KMS infrastructure
RMSYIELD lookup controller 3 Branching logic for casino vs non-casino
Security configuration 1 New filter chain
Integration testing 2-3 OPERA Cloud RMSYIELD sandbox
Total 11-12 days

Future Optimization (Not v1)

For casino properties with DRE that need sub-10ms latency: - Push yield market config to DRE (new upsert endpoint) - DRE serves RMSYIELD directly using local MongoDB + Memcached loyalty cache - Monolith config routes RMSYIELD traffic to DRE URL instead of self

Source Documents