C-MERCH — Hospitality Merchandising Patterns
Scope: A pattern library for surfacing offers, urgency signals, trust cues, and loyalty affordances in the consumer booking funnel, property detail pages, and guest portal. Reusable across
meta-web,tenant-booking-web, andguest-portal-web. All patterns include ethical guardrails — no dark patterns; accuracy is required.
Ethics statement: Melmastoon does not implement manipulative scarcity (fabricated "only 2 left"), fake social proof, or deceptive urgency timers. Every pattern below includes an accuracy requirement. Violation is grounds for removing the pattern from a tenant's configuration.
Pattern index
| Pattern | Code name | Surfaces |
|---|---|---|
| Rate plan card | RatePlanCard | Tenant booking web (room selection) |
| Upsell strip | UpsellStrip | Tenant booking web (room selection, checkout), Guest portal |
| Cross-sell carousel | CrossSellCarousel | Tenant booking web (confirmation), Meta web |
| Promo / discount surfacing | PromoEntry | Tenant booking web (guest details, payment) |
| Loyalty card | LoyaltyCard | Guest portal, consumer mobile |
| Trust badges | TrustBadges | All booking surfaces |
| Social proof | SocialProof | Meta web (property detail), tenant booking web |
| Urgency indicator | UrgencyIndicator | Meta web (search results), tenant booking web |
| Reviews display | ReviewsDisplay | Meta web (property detail), tenant booking web |
1. Rate plan card (RatePlanCard)
Context: Displayed in the room selection step of the booking funnel. One card per available rate plan (e.g., "Flexible", "Non-refundable", "Breakfast included").
Anatomy
┌────────────────────────────────────────────────┐
│ [RatePlanName] [Price/night]│
│ [CancellationPolicyBadge] [Total] │
│ [InclusionsList] │
│ ✓ Free cancellation until [date] │
│ ✓ Breakfast included │
│ ✗ Non-refundable (greyed, strikethrough) │
│ [Select rate] │
└────────────────────────────────────────────────┘
Variants
| Variant | When |
|---|---|
highlighted | Best-value rate (platform-computed by room-availability-service) |
dimmed | Sold-out or restricted rate |
member-rate | Lower price visible only to loyalty members |
Token fields
| Field | Type | Source |
|---|---|---|
rate_plan_id | string | room-availability-service |
name | LocaleString | Tenant-configured |
price_per_night | Money | BFF; live pricing |
total_price | Money | BFF; includes taxes + fees |
cancellation_policy | 'free' | 'partial' | 'non-refundable' | Rate plan config |
free_cancellation_until | ISO8601 | null | Rate plan config |
inclusions | LocaleString[] | Rate plan config |
is_member_rate | boolean | BFF; requires auth |
Ethics: cancellation badge
'free'→ Green badge "Free cancellation until [date]"'partial'→ Amber badge "Partial refund — see policy"'non-refundable'→ Red badge "Non-refundable"
Badge must be visible without scrolling within the card. Never hide the cancellation policy behind a tooltip-only affordance.
2. Upsell strip (UpsellStrip)
Context: Displayed after room selection (pre-payment) and in the guest portal (mid-stay). Offers add-ons relevant to the guest's stay.
Anatomy
Enhance your stay
──────────────────────────────────────────────
[UpsellItem] [UpsellItem] [UpsellItem] [→]
🌅 Late 🍳 Breakfast 🚘 Airport
Checkout + $12/pp Transfer
+ $30 [Add] + $45 [Add]
──────────────────────────────────────────────
Upsell item types (hospitality-standard)
| Item | Icon | Price model |
|---|---|---|
| Room upgrade | bed glyph | Per stay |
| Late checkout | clock | Per stay |
| Early check-in | sunrise | Per stay |
| Breakfast | fork-knife | Per person per night |
| Airport transfer | car | Per direction |
| Spa treatment | leaf | Per session |
| Parking | parking | Per night |
| F&B room credit | wine | Fixed credit |
| Welcome amenity | gift | Per stay |
Rules
- Maximum 5 upsell items shown at once (pagination if more available)
- Items shown are personalised by
ai-gateway-service→upsell-ranking-model(Phase 2 — R1 shows all available items sorted by margin) - Prices are live from
folio-service; not cached (staleness = incorrect charges) - "Added" state: item card flips to a confirmation state with "Remove" option
- No auto-add: guest must explicitly tap "Add"; pre-checked checkboxes are prohibited
Guest portal mid-stay context
Only show upsells relevant to remaining stay duration (e.g., don't show "late checkout" if guest checks out today).
3. Cross-sell carousel (CrossSellCarousel)
Context: Shown on the booking confirmation page ("You might also like") and on the meta-web property detail page.
Anatomy
Other properties you might enjoy
─────────────────────────────────────────────────────
[PropertyCard] [PropertyCard] [PropertyCard] [→]
Each PropertyCard in a cross-sell context shows: thumbnail, property name, city, lowest-available rate, overall rating.
Rules
- Cross-sell properties are served by
search-aggregation-serviceusing guest's stay dates and destination - Never show the same property the guest just booked
- "Partner properties" label if any cross-sell is a commercial placement (paid ranking boost)
- Maximum 6 items; horizontal scroll on mobile
4. Promo / discount surfacing (PromoEntry)
Context: In the booking funnel at the guest-details or payment step.
Anatomy
Have a promo code?
[_________________] [Apply]
✓ Code SUMMER25 applied — 25% off your stay
New total: $270 (was $360)
Variants
| Variant | Description |
|---|---|
coupon-entry | Free-text promo code input |
member-rate-reveal | "Sign in to see your member rate" CTA |
mobile-exclusive | Banner: "You're getting our mobile-only rate" |
partner-rate | "Booked via [OTA name] — contracted rate applied" |
Rules
- Discount amount shown in absolute value AND percentage (e.g., "You save $90 (25%)")
- If a promo code is invalid: inline error "This code is invalid or expired" (not a modal)
- If a better rate (member) is available, surface a non-intrusive banner: "Log in to save [X%]" — not an obstruction
- Never auto-apply a discount without confirmation (ghost discounts confuse folios)
5. Loyalty card (LoyaltyCard)
Context: Consumer mobile (account section), guest portal (home).
Anatomy
┌──────────────────────────────────────────────┐
│ 🌟 Melmastoon Member │
│ [TierBadge: Silver] │
│ │
│ 1,240 pts Next tier: Gold at 2,000 pts │
│ ████████░░░░░░ 62% │
│ │
│ Perks: │
│ ✓ Free breakfast from Gold tier │
│ ✓ Late checkout (on request) │
│ ✓ 10% off spa │
│ [View all] │
└──────────────────────────────────────────────┘
Rules
- Points balance is real-time from
loyalty-service(not cached) - Progress bar is accessible:
role="progressbar" aria-valuenow={62} aria-valuemin={0} aria-valuemax={100} aria-label="62% to Gold tier" - Tier benefits list: max 3 shown; "View all" expands
- Phase 2: animated confetti on tier upgrade
6. Trust badges (TrustBadges)
Context: All booking surfaces — typically near the "Book now" / "Pay" CTA.
Badge set (standard)
| Badge | Icon | Condition for display |
|---|---|---|
| Verified property | shield-check | Property passed platform verification |
| Free cancellation | arrow-counter-clockwise | Rate plan has cancellation: 'free' |
| Secure payment | lock-simple | Always shown on payment step |
| Best-price guarantee | tag | Tenant opts in (platform must back with price-match policy) |
| Instant confirmation | lightning | Rate plan has confirmation: 'instant' |
| Travel insurance | umbrella | If insurance upsell available |
Rules
- Maximum 4 badges shown horizontally (overflow → hide, not carousel)
- Each badge has a
title/aria-labelexplaining it (e.g., "Secure payment: Your card details are encrypted") - "Best-price guarantee" badge requires an ADR note in the tenant's config confirming the price-match policy is active
- No fabricated badges ("Award winner 2026" without evidence)
7. Social proof (SocialProof)
Context: Meta-web property detail page, tenant booking web home block.
Sub-patterns
7a. Recent bookings count
🔥 47 people booked this property in the last 7 days
Accuracy requirement: Real data from analytics-service. Minimum threshold: only display if count ≥ 5 in the time window. If count < 5, hide the element entirely (don't show "2 people booked...").
7b. Currently viewing count
👁️ 12 people are looking at this property right now
Accuracy requirement: Real-time presence count from channel-service. Minimum threshold: ≥ 3 concurrent viewers. Count updates every 60 s.
7c. Review snippet
A single highlighted review (highest-rated, most-recent, or most-helpful — configurable):
"The breakfast was amazing and the staff were so helpful."
— Ahmad K., verified stay, March 2026 ★★★★★
Rules:
verified staybadge only for reviews from guests who completed a stay via the platform- Review text truncated at 140 chars with "Read more" expansion
- "Helpful" vote count shown if ≥ 5 votes
8. Urgency indicator (UrgencyIndicator)
Context: Meta-web search results (property card), tenant booking web (rate plan card).
Sub-patterns
8a. Room scarcity
⚡ Only 2 rooms left at this rate
Accuracy requirement: Real-time from room-availability-service. Only display when available_rooms ≤ 3 for the selected dates. If availability changes between render and display (> 5 min), the badge auto-refreshes. Never hardcode.
8b. Price trend
📈 Price increased 15% since yesterday
Accuracy requirement: analytics-service provides yesterday's average rate. Only show if difference ≥ 10%.
Direction rule: Only show "price increased" — never show "price decreased" as urgency (it's not urgent; it's a benefit, and it belongs in a discount badge).
8c. High-demand badge
🔥 Popular this weekend
Accuracy requirement: Shown when occupancy forecast (from analytics-service) for the searched dates is > 80% for comparable properties in the same destination.
Ethical guardrails (all urgency patterns)
- All urgency data must be real-time or sourced from actual historical data
- Fabricating urgency signals is a platform policy violation (tenant config suspended)
- Urgency signals must be removable per tenant if they opt out
- Never pair multiple urgency signals on the same element simultaneously (max one)
9. Reviews display (ReviewsDisplay)
Context: Meta-web property detail, tenant booking web.
Anatomy
Overall rating: ★★★★☆ 4.2 / 5 (138 reviews)
Rating breakdown:
Cleanliness ████████░░ 4.5
Staff █████████░ 4.8
Location ██████░░░░ 3.9
Value ███████░░░ 4.1
[Filter by: Most recent ▾] [Sort by: Most helpful ▾]
── Review card ──────────────────────────────────────
Ahmad K. ★★★★★ March 2026 [Verified stay]
"Excellent hospitality..."
[Response from owner: "Thank you Ahmad! ...]
[Helpful (12)] [Not helpful (1)]
─────────────────────────────────────────────────────
[Load 10 more reviews]
Rules
- Rating breakdown bars:
role="meter"witharia-valuenow,aria-valuemin,aria-valuemax,aria-label - Verified stay badge: only for platform-verified bookings (same rule as
7c) - "Response from owner" shown when present (expands inline, not modal)
- "Helpful" vote: one vote per user per review; requires auth (magic link for guest portal; full auth for others)
- Minimum 3 reviews to show the aggregate rating; below 3, show "No reviews yet — be the first"
- No review suppression: tenants cannot hide individual negative reviews (platform policy)
RTL notes (all patterns)
- Scarcity count: "Only 2 left" → in Pashto:
يوازې ۲ پاتې دي— Latin numerals per F4 invariant? Exception: Room counts and days remaining use Latin numerals (quantities, not prices) per09-NFR.md §3— Latin for quantities in financial context; locale numerals permissible for non-financial counts in body copy per tenant locale-pack. Implement with<NumericDisplay context="count">which reads the tenant'snumeral_variantsetting. - Layout: all patterns mirror in RTL (flex-row-reverse, icon positions swap)
- Trust badges: text and icons align to
inline-start
Accessibility notes (all patterns)
- Urgency indicators: not conveyed by color alone (include icon + text)
- Rating stars:
aria-label="4.2 out of 5 stars"on the star visual; do not use ★ characters as the only rating cue - Progress bars (loyalty, rating breakdown): always
role="meter"with fullaria-value*attributes - Upsell strip: horizontal scroll region has
role="region" aria-label="Add-on options"; keyboard accessible (Tab through items, arrow keys scroll)
Telemetry events
| Event | Trigger | Properties |
|---|---|---|
rate_plan.viewed | Rate plan card rendered above fold | rate_plan_id, price, cancellation_type |
rate_plan.selected | Guest selects a rate | rate_plan_id |
upsell.impression | Upsell strip rendered | items_shown: string[] |
upsell.item_added | Guest adds an upsell | item_type, price |
promo.applied | Promo code successfully applied | discount_pct |
social_proof.booking_count.shown | Booking count rendered | count, window_days |
urgency.scarcity.shown | Scarcity badge rendered | rooms_remaining |
reviews.loaded | Review list rendered | count, average_rating |
reviews.helpful_vote | User votes helpful | review_id |