OPERA Cloud Yield Market Lookup
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
- Expose a synchronous REST API conforming to OPERA Cloud's RMSYIELD outbound specification (OAuth2 + Yield Market Lookup)
- Support casino properties via the existing loyalty tier bucket chain (membershipId → patron score → segment → yield market code)
- Support non-casino properties via a new static YieldMarketMapping configuration (membershipId → pattern match → yield market code)
- Reuse existing monolith infrastructure: JWT/KMS auth, SecurityFilterChain, MongoDB patterns
- 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 theuicomponent today uicomponent is stable — the 93-99% heap / 30-60s GC pauses are onappBatchservers;uihandles 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/:
- Validate OAuth2 bearer token
- Resolve hotelId → Hotel
- Branch on LoyaltyProgram presence:
- Casino path: LoyaltyScoreProvider → TierBucket match → read
yieldMarketCode+guestValueLabel - Non-casino path: YieldMarketMapping → pattern match → read mapping
- 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
- Yield Market Lookup (1).pdf — OPERA Cloud configuration guide
- OPERACloud_YTM_Lookup V2.pptx — Business case and API specification