Offer

canonical term: offer | plural: offers This document explains the Offer concept — the single product model that powers House of Legends’ booking catalog.
Doc Status: Current code alignment pass. Verified against seeder offer payloads, Hono offer persistence, public catalog mapping, reservation pricing, and frontend booking/admin consumers.

Why This Matters

Offers have accumulated fields from seeding, booking, admin configuration, and older compatibility paths. The useful question is not “what columns exist?” but “which fields change runtime behavior, guest display, pricing, or operations today?” This page separates the fields that are actively meaningful from fields that are only persisted for compatibility or seed transport.

What Is an Offer?

An Offer is a sellable item in the booking catalog. It has:
  • Identity — stable database identity and human-readable references
  • Scope — whether the offer belongs to one show or is globally available
  • Eligibility Conditions — rules that determine when the offer appears
  • Commercial Terms — price, reference price, quantity limits, and discount display
  • Presentation — copy, image, ordering, and default/recommended flags
  • Fulfillments — what the guest actually receives (food, drink, souvenir, seat)
  • Lifecycle State — active/paused/validity windows
The offer is what appears in Choose Experience — the section where guests choose their experience enhancements. The same record is also used by reservation pricing, admin offer configuration, fulfillment planning, and demo/reservation seeding.

Fields Actually Used Today

The active runtime surface is smaller than the database table. These are the fields that currently matter.
FieldRuntime meaning
idSelected by guests and stored on reservations as offerId. Also used by admin saves and fulfillment-impact checks.
slug / API referenceStable human reference. Seeder writes ref = slug; admin config exposes it as reference; upserts use it as the business key.
scopeSplits SHOW offers attached to a show from GLOBAL offers. Public catalog nests show-scoped offers under each show and returns global offers separately.
showIdRequired for meaningful SHOW offers. Catalog skips show-scoped offers with missing or unknown showId.
kindControls pricing and selection semantics: BASE_ADMISSION, PACKAGE, OPTIONAL, or MENU_ITEM. Reservation pricing handles these differently.
conditionsThe eligibility rule set. BOOKING_PATH, GUEST_COUNT, and EVENT_DAY are the currently supported public/admin condition types. This is the canonical place for offer eligibility.
name / API labelGuest-facing and operator-facing title. Public catalog uses name; admin config maps it as label.
descriptionGuest-facing card copy and operator-facing description.
priceThe offer price in VND. Reservation pricing multiplies it by selection quantity when applicable.
referencePrice / API aLaCartePrice / public compareAtPriceCompare-at or before-discount value. Used for admin/comparison display, not as the charged total.
maxQuantityReservation pricing validates selected quantity against this value when it is present.
active, isPaused, validFrom, validToRuntime sellability gates. Catalog and reservation pricing filter inactive, paused, future, and expired offers.
displayOrderCatalog/admin ordering.
positioningLabelShort merchandising/operator label displayed in admin and available to UI.
imageUrl / API thumbnailOffer image asset key. Backend resolves it to public R2 URLs before frontend display.
discountPercentDisplay/config metadata for bundles. Current reservation discount totals come from promo/pricing logic, not this field.
isRecommendedActive UI control. Dinner-show default pack selection and booking badges use this flag; VIP also prefers the recommended active offer.
isDefaultStored and exposed, but current booking default selection uses isRecommended, not isDefault. Treat as secondary/legacy until a consumer is added.
fulfillments[]Operational payload: what the offer includes, quantities, display order, guest visibility, station/category data, and prep metadata.
Do not add new offer logic to duplicated eligibility fields. conditions[] is the source of truth; duplicate fields should be removed from authored offer data and migrated out of consumers.

Duplicate Fields To Remove

These fields exist in persistence, seeding payloads, or API aliases today, but they duplicate another source of truth. Do not treat them as acceptable long-term model shape.
FieldCurrent status
bookingPathDuplicate of conditions[] where type = "BOOKING_PATH". Remove from authored offer seed data, admin payloads, catalog DTOs, and downstream consumers.
guestCountDuplicate of a GUEST_COUNT condition when it means eligibility. If a guest-count tier is needed for display, derive it from conditions[] instead of storing a second field.
applicablePathsDuplicate of BOOKING_PATH conditions for optional/global add-ons. Replace with conditions[].
minGuests / maxGuestsDuplicate of GUEST_COUNT conditions for optional/global add-ons. Replace with conditions[].
refDuplicate of slug for offers. Keep one stable human key: slug in storage and reference only as an API label if needed.
visibilityActiveOverlaps with active, isPaused, validFrom, and validTo. Keep one lifecycle model unless a separate visible-but-not-sellable state is explicitly implemented.
isDefaultOverlaps with current isRecommended-based preselection. Either wire it as the sole default-selection field or remove it.
createdAt / updatedAtAudit timestamps only.
bookingPath should not remain in the canonical offer model. Current code may still read it during migration, but new work should move those reads to conditions[] rather than keeping both fields synchronized.

Offer Structure

Every offer is composed of these parts:
offer
├── identity         — id, slug, ref
├── scope            — SHOW or GLOBAL, plus showId when show-scoped
├── conditions[]     — eligibility rules: booking path, guest count, event day...
├── commercial terms — price, referencePrice, maxQuantity, discountPercent
├── presentation     — name, description, imageUrl, displayOrder, badges/flags
├── lifecycle        — active, isPaused, validFrom, validTo
└── fulfillments     — what the guest receives through linked fulfillment items

Field Ownership

This section defines ownership by current code path.
OwnerFields
Backend identity and persistenceid, slug, ref, scope, showId, createdAt, updatedAt
Pricing and reservation validationid, kind, price, maxQuantity, active, isPaused, validFrom, validTo, conditions, showId
Catalog eligibility and groupingscope, showId, kind, conditions, displayOrder
Guest presentationname, description, price, referencePrice, positioningLabel, imageUrl, isRecommended, fulfillments[]
Operator/admin configurationAPI reference, showId, conditions, label, positioningLabel, description, thumbnail, price, aLaCartePrice, discountPercent, active, isRecommended, isPaused, validFrom, validTo, fulfillments[]
For package and bundle offers, new eligibility behavior belongs only in conditions[]. Do not mirror it into bookingPath, guestCount, applicablePaths, minGuests, or maxGuests.

Offer Metadata

Metadata defines the identity and display properties of an offer. These are top-level fields on the offer record — not stored in the conditions arrays.
FieldTypeDescription
namestringDisplay name, e.g. "The Solo Night"
slugstringURL-safe kebab-case identifier, e.g. "the-solo-night"
descriptionstringLong-form description shown on the offer detail
imageUrlstringAsset key or resolved public image URL for the offer card
isDefaultbooleanWhether this offer is pre-selected when the step loads
isRecommendedbooleanWhether this offer is highlighted in the Choose Experience comparison UI
pricenumberPrice in VND — a single value, not conditional

Offer Conditions

Offer conditions determine when an offer is eligible to appear. They are stored as a JSON array conditions[] on the offer. An offer is shown to the guest only when all its conditions pass. Each condition has a type field that determines what other fields are present:

BOOKING_PATH

Restricts the offer to a specific booking path.
FieldValue
typeBOOKING_PATH
operatorIN
values["TICKETS_ONLY"], ["DINNER_SHOW"], or ["VIP"]

GUEST_COUNT

Restricts the offer to a specific guest count range.
FieldValue
typeGUEST_COUNT
operatorBETWEEN
minnumber (e.g. 1)
maxnumber (e.g. 1)
Use { min: 1, max: 1 } for solo offers, { min: 4, max: 4 } for table-of-4 offers, etc.

EVENT_DAY

Restricts the offer to specific days of the week.
FieldValue
typeEVENT_DAY
operatorIN
values["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
Use for offers that are only available on certain nights (e.g. a weekend-only promotion).

Condition Evaluation

Conditions are evaluated by the offer-condition-evaluator.service.ts at render time. The context passed in includes:
  • bookingPath — the guest’s chosen booking path
  • guestCount — number of guests selected
  • eventDate — the date of the show event
All conditions must pass for the offer to be eligible. If any condition fails, the offer is hidden.

How Offers Relate to Fulfillments

Offer is what the guest pays for. Fulfillment is what the guest receives. An offer is composed of one or more fulfillments:
"The Solo Night" (offer)
  ├── fulfillment: Chicken Popcorn + Onion Rings (fixed, per party)
  ├── fulfillment: Signature Cocktail (choice, per guest)
  └── fulfillment: Still Water (fixed, per party)
The fulfillment breakdown drives kitchen prep counts and bar stock. The kitchen needs to know: for tonight’s show, how many Sizzling Beef Skillets? How many cocktails?

Offer and the Booking Flow

At Choose Experience of the booking flow, the guest sees offers relevant to their booking:
  • TICKETS_ONLY path — shows Ticket Only offers filtered by guest count (3 cards shown)
  • DINNER_SHOW path — shows Dinner Show offers filtered by guest count
The system’s offer-condition-evaluator filters the full catalog down to eligible offers based on the guest’s booking context. Backend computes the total. Frontend displays it. Frontend never recalculates.

See Also