food.book_dine_in_with_offer — Full Intent Specification
INTENT NAMESPACE: food
INTENT NAME: book_dine_in_with_offer
FULL ID: food.book_dine_in_with_offer
VERSION: v1.0.0
STATUS: draft
LAST UPDATED: 2026-05-10
TTBS WEIGHTS: time 0.20 · taste 0.40 · budget 0.30 · safety 0.10
Dine-in with offer is the offer-aware variant of food.book_dine_in. The full restaurant + slot shape from food.book_dine_in is INHERITED — every field of Restaurant, SlotOption, RestaurantDetail, Reservation is REQUIRED here too. This spec extends it with: (a) offer_search_criteria input block; (b) OfferOption[] first-class on every restaurant result; (c) compute_offer_quote + redeem_offer_voucher tools; (d) offer-aware TTBS BUDGET (offer-discounted total drives ranking, not headline price); (e) anti-fabrication rules around offer authenticity (TOMO never funds, no fake savings %, no misleading code-required text). Budget weight rises to 0.30; safety drops to 0.10 (a vetted pattern: user is OK with offer-discovery + accepts the listed restaurants).
Inheritance contract: every field listed in food.book_dine_in §2/§4/§5 is REQUIRED here. This spec lists ONLY the deltas. Partners that implement this intent MUST also implement food.book_dine_in end-to-end.
1. NATURAL LANGUAGE COVERAGE
Classifies IN
- "table at Paradise with offer"
- "dinner with deal tonight"
- "restaurants with discount near me"
- "30% off restaurants in Banjara Hills"
- "buy one get one dinner deal"
- "Zomato Gold restaurants near me"
- "EazyDiner prime time deal"
- "happy hours bar with offer"
- "early bird dinner discount Saturday"
Classifies OUT — borderline NO
- "table for 4 at Paradise tonight" (no offer mentioned) →
food.book_dine_in - "biryani for dinner" →
food.order_delivery - "tiffin subscription" →
food.subscribe_tiffin - "catering with discount" →
food.book_catering_event
MULTI-INTENT TRIGGERS
- "EazyDiner prime time and a cab" →
food.book_dine_in_with_offer+mobility.book_intracity_ride - "BOGO dinner and a hotel near MG Road" →
food.book_dine_in_with_offer+travel.book_hotel - "Zomato Gold dinner and an anniversary cake" →
food.book_dine_in_with_offer+food.order_cake_or_special
2. INPUT — TOMO → PROVIDER (DELTA over food.book_dine_in)
All fields from food.book_dine_in §2 are REQUIRED. ADD the following block:
{
"offer_search_criteria": {
"min_savings_pct": 20,
"min_savings_inr": 300,
"offer_kinds_acceptable": ["flat_pct_off", "bogo", "free_dish_with_min_cart", "prime_time", "happy_hours", "voucher_redemption"],
"voucher_already_held": false,
"voucher_code": "",
"loyalty_program_membership_kind": "none",
"loyalty_member_id": "",
"use_partner_wallet_credit": false,
"partner_wallet_credit_inr": 0,
"auto_apply_best_offer": true
}
}
| Field | Type | Constraint | Notes |
|---|---|---|---|
offer_search_criteria.min_savings_pct |
int | REQUIRED, 0-100 | floor; TOMO drops options below |
offer_search_criteria.min_savings_inr |
INR_INTEGER | REQUIRED, ≥0 | absolute floor |
offer_search_criteria.offer_kinds_acceptable |
array |
REQUIRED, ≥1 | see §5 |
offer_search_criteria.voucher_already_held |
bool | REQUIRED | drives voucher redemption flow |
offer_search_criteria.voucher_code |
string | REQUIRED, may be empty | only relevant if voucher_already_held |
offer_search_criteria.loyalty_program_membership_kind |
enum | REQUIRED, see §5 | |
offer_search_criteria.loyalty_member_id |
string | REQUIRED, may be empty | |
offer_search_criteria.use_partner_wallet_credit |
bool | REQUIRED | |
offer_search_criteria.partner_wallet_credit_inr |
INR_INTEGER | REQUIRED, ≥0 | |
offer_search_criteria.auto_apply_best_offer |
bool | REQUIRED | when true, partner picks best |
Anti-fabrication preamble (universal): no paid placement, no urgency text, no commission-influenced fields. Plus: TOMO never funds offers (see §12 Rule 1).
3. PROVIDER TOOLS (DELTA)
All 8 tools from food.book_dine_in §3 are REQUIRED. ADD:
Tool 9: search_dine_in_with_offers
PURPOSE: replaces search_dine_in_options when offer-aware filter is on
INPUT: §2 (base + offer_search_criteria block)
OUTPUT: { results: DineInOfferOption[], result_token, expires_at }
SLA: p50 < 700ms, p95 < 1500ms
RATE LIMIT: ≤ 1/sec per user
Tool 10: compute_offer_quote
PURPOSE: compute pre/post-offer pricing for a specific slot + offer
INPUT: { restaurant_id, slot_id, offer_id, party_size, request_id }
OUTPUT: OfferQuote (§4)
SLA: p95 < 600ms
USE: called whenever user toggles offer choice
Tool 11: redeem_offer_voucher
PURPOSE: validate + lock a held voucher to the booking
INPUT: { restaurant_id, slot_id, voucher_code, request_id, idempotency_key }
OUTPUT: { acknowledged: true, voucher_locked_until_iso, savings_inr }
SLA: p95 < 1500ms
IDEMPOTENCY: REQUIRED on idempotency_key
All 11 tools REQUIRED end-to-end.
4. RESPONSE SHAPE (DELTA)
DineInOfferOption (replaces DineInOption for this intent)
All fields of DineInOption from food.book_dine_in §4 are REQUIRED. ADD:
offers: array<OfferOption>, REQUIRED, ≥1
best_offer_id: string, REQUIRED # auto-selected per max savings
best_offer_savings_inr: INR_INTEGER, REQUIRED, ≥0
best_offer_post_discount_per_head: INR_INTEGER, REQUIRED
loyalty_program_recommended: STRICT ENUM, REQUIRED # see §5; none if none applies
OfferOption
offer_id: string, REQUIRED
offer_kind: STRICT ENUM, REQUIRED # see §5
title: string, REQUIRED # "30% off on dinner buffet"
description: string, REQUIRED # human-readable, ≥80 chars
applicable_slot_ids: array<string>, REQUIRED, ≥1 # which slot_ids this offer applies to
discount_kind: STRICT ENUM, REQUIRED # see §5
discount_value: float, REQUIRED # 100 = ₹100 flat OR 30 = 30%
min_cart_inr: INR_INTEGER, REQUIRED
max_discount_inr: INR_INTEGER, REQUIRED
estimated_savings_inr: INR_INTEGER, REQUIRED # calculated for this party
estimated_savings_pct: float, REQUIRED, 0-1
valid_from_iso: ISO_DATETIME, REQUIRED
valid_until_iso: ISO_DATETIME, REQUIRED
applicable_party_size_min: int, REQUIRED, ≥1
applicable_party_size_max: int, REQUIRED # 0 means no upper cap
applicable_meal_periods: array<STRICT ENUM>, REQUIRED, ≥1
combinable_with_other_offers: boolean, REQUIRED
auto_applied: boolean, REQUIRED
voucher_code_required: boolean, REQUIRED
voucher_code: string, REQUIRED # "" if not required
loyalty_membership_required: STRICT ENUM, REQUIRED # see §5; none if none
funder: STRICT ENUM, REQUIRED # restaurant | partner | loyalty_program
# NEVER tomo
funder_disclosure_text: string, REQUIRED # who paid for this discount
restrictions_text: string, REQUIRED # "Mon-Thu only", "Excludes festive days", etc.
limit_per_user_total: int, REQUIRED # 0 = unlimited
limit_per_user_period: STRICT ENUM, REQUIRED # see §5
inventory_limit: int, REQUIRED, ≥-1 # -1 = unlimited; otherwise total redemptions allowed
inventory_remaining: int, REQUIRED, ≥-1
verified_at_partner_side: boolean, REQUIRED # partner has run validation
OfferQuote (returned by compute_offer_quote)
restaurant_id: string, REQUIRED
slot_id: string, REQUIRED
offer_id: string, REQUIRED
pre_offer_per_head_inr: INR_INTEGER, REQUIRED
post_offer_per_head_inr: INR_INTEGER, REQUIRED
total_pre_offer_inr: INR_INTEGER, REQUIRED
total_post_offer_inr: INR_INTEGER, REQUIRED
savings_inr: INR_INTEGER, REQUIRED, ≥0
savings_pct: float, REQUIRED, 0-1
deposit_inr: INR_INTEGER, REQUIRED # may differ post-offer
deposit_redeemable_against_post_offer_meal: boolean, REQUIRED
loyalty_points_earned: int, REQUIRED # 0 if not loyalty
loyalty_points_used: int, REQUIRED # if redeemed at booking
voucher_locked: boolean, REQUIRED
voucher_locked_until_iso: ISO_DATETIME, REQUIRED
quote_valid_until_iso: ISO_DATETIME, REQUIRED
gst_inr: INR_INTEGER, REQUIRED
service_fee_inr: INR_INTEGER, REQUIRED
fees_total_inr: INR_INTEGER, REQUIRED
final_quote_text: string, REQUIRED # human-readable breakdown
Forbidden fields (additions over food.book_dine_in)
fake_savings_pct | fake_inventory_remaining | undocumented_funder |
hidden_voucher_redemption_charge | tomo_funded (always false; literal string "tomo" forbidden in funder)
5. CONTROLLED VOCABULARIES (DELTA)
All vocabularies from food.book_dine_in §5 are REQUIRED.
offer_search_criteria.offer_kinds_acceptable[] and OfferOption.offer_kind
flat_pct_off | flat_inr_off | bogo | free_dish_with_min_cart |
prime_time | happy_hours | early_bird | weekend_special |
voucher_redemption | loyalty_redemption | loyalty_member_only_pct |
combo_pricing | tasting_menu_special | chef_special_offer
OfferOption.discount_kind
flat_inr | pct | bogo | free_item | bundle | min_cart_threshold |
loyalty_points_redemption | voucher_value | tier_pricing
OfferOption.applicable_meal_periods[]
breakfast | brunch | lunch | tea_time | snacks | dinner | late_night | open_all_day
OfferOption.funder
restaurant | partner | loyalty_program
tomo is FORBIDDEN as a value here. TOMO never funds offers. Any offer labelled tomo-funded is automatically rejected at TOMO ingest.
OfferOption.loyalty_membership_required and offer_search_criteria.loyalty_program_membership_kind
none | zomato_gold | dineout_passport | eazydiner_prime |
swiggy_one | magicpin_pulse | partner_specific_program
OfferOption.limit_per_user_period
once_only | per_day | per_week | per_month | per_year | per_lifetime
DineInOfferOption.loyalty_program_recommended
Same enum as OfferOption.loyalty_membership_required.
cancel_reservation.reason (additions)
offer_invalid | offer_inventory_exhausted_post_quote | voucher_already_redeemed |
... (all base reasons from food.book_dine_in)
6. TTBS DIMENSIONS (DELTA)
All TIME / TASTE / SAFETY signals from food.book_dine_in §6 are REQUIRED. BUDGET is overridden:
Per-domain weights (override)
food (book_dine_in_with_offer): { time: 0.20, taste: 0.40, budget: 0.30, safety: 0.10 }
Budget rises to 0.30 (offer is the explicit ask). Safety drops to 0.10 — user has accepted offer-discovery surface; HARD FILTERS still apply for fssai/alcohol/wheelchair/etc.
BUDGET (override)
SIGNALS USED:
- post_offer_per_head_inr vs band:
ok → 0–33rd percentile (cuisine, city, post-offer)
good → 33rd–66th
great → 66th+
- estimated_savings_pct (higher = better) weight 0.30
- estimated_savings_inr (higher = better) weight 0.20
- offers[].auto_applied (no friction = better) weight 0.10
- SlotOption.deposit_inr (lower=better) weight 0.10
- SlotOption.cover_charge_inr (lower=better) weight 0.10
- amenities.valet_charge_inr (if valet required) weight 0.10
- offer.combinable_with_other_offers (more choice) weight 0.10
HARD FILTERS:
- estimated_savings_pct < min_savings_pct → drop offer (not restaurant)
- estimated_savings_inr < min_savings_inr → drop offer
- all offers dropped + no base option → drop restaurant
- post_offer_per_head_inr > preferences.budget_max_inr_per_head → drop
USER BAND HANDLING:
- "biggest discount" intent → savings_pct weight 0.45
- "premium experience with deal" → savings_pct weight 0.20, taste back to 0.40
Hidden ranking factor
information_completeness_score weight 0.10 (same as base intent).
7. COMPLETION CONTRACT (DELTA)
Same shape as food.book_dine_in §7. ADD these fields to the POST body:
POST /api/v1/cpc/mcp_provider/{tomo_partner_id}
X-TOMO-Timestamp: <ms>
X-TOMO-Signature: sha256=<hex>
{
"intent": "food.book_dine_in_with_offer",
... (all base fields from food.book_dine_in §7) ...
"offer_id": "EAZYDINER-PRIMETIME-XYZ",
"offer_funder": "partner",
"pre_offer_total_inr": 5500,
"post_offer_total_inr": 3850,
"savings_inr": 1650,
"savings_pct": 0.30,
"voucher_code_used": "PRIMETIME30",
"loyalty_points_earned": 385,
"loyalty_points_used": 0
}
Status enum (same as base): completed | cancelled_by_user | cancelled_by_restaurant | no_show | failed | partial_completion_user_left_early
8. WIDGET (DELTA)
WIDGET TYPE: dine_in_with_offer_options
SOURCE: src/widgets/types.ts
TYPE NAME: DineInOfferOptionsPayload
RENDERED IN: components/widgets/DineInOfferOptionsWidget.tsx
Default: 3 stacked rows with savings_pct badge prominent (e.g., "30% off"), restaurant + cuisine, slot pill, post-offer per-head price strikethrough vs pre-offer. Tap row → restaurant detail card with offer carousel (each offer expandable to show terms + savings) → "Book with offer". Voucher-required offers route through redeem_offer_voucher inline.
9. CACHING POLICY
| Call | TTL | Rationale |
|---|---|---|
search_dine_in_with_offers |
60s | offers turn over fast |
compute_offer_quote |
30s | quote_valid_until_iso enforced too |
redeem_offer_voucher |
0s | always live |
| All base tools | per food.book_dine_in §9 |
inherited |
| Failure responses | 0s |
10. ERROR CODES (DELTA)
All codes from food.book_dine_in §10 are REQUIRED. ADD:
| Code | HTTP | Meaning | TOMO behavior |
|---|---|---|---|
OFFER_INVALID |
400 | offer_id no longer valid | re-quote |
OFFER_INVENTORY_EXHAUSTED |
409 | offer used up | re-quote with alternates |
VOUCHER_INVALID |
400 | voucher_code rejected | surface |
VOUCHER_EXPIRED |
410 | voucher past valid_until | surface |
VOUCHER_ALREADY_REDEEMED |
409 | duplicate redemption | surface |
LOYALTY_NOT_ELIGIBLE |
403 | user's loyalty tier doesn't qualify | surface |
OFFER_NOT_COMBINABLE |
400 | tried to stack two non-combinable | surface |
MIN_CART_NOT_MET_FOR_OFFER |
400 | cart below offer min_cart_inr | surface upsell |
11. SANDBOX → PRODUCTION CHECKLIST (DELTA)
All checklist items from food.book_dine_in §11 are REQUIRED. ADD:
[ ] search_dine_in_with_offers returns ≥10 restaurants with active offers
[ ] All OfferOption fields populated with real, verifiable offer data
[ ] funder field NEVER set to "tomo"
[ ] estimated_savings_pct accurate within ±2pp vs computed savings
[ ] estimated_savings_inr accurate within ±5 INR
[ ] inventory_remaining decrements after each redemption
[ ] auto_applied offers compute correctly without voucher_code
[ ] voucher_code_required flow tested end-to-end
[ ] redeem_offer_voucher returns voucher_locked_until_iso
[ ] loyalty_membership_required offers gate correctly per loyalty_member_id
[ ] limit_per_user_total + limit_per_user_period enforced server-side
[ ] CPC webhook includes offer_id + savings_inr + funder
[ ] No paid_placement: offers ranked by max(savings_inr) + match score, NEVER by funder commission
[ ] funder_disclosure_text accurate and user-visible
[ ] No artificial scarcity in inventory_remaining (TOMO 1% audit)
12. ANTI-FABRICATION RULES (DELTA)
All rules from food.book_dine_in §12 are REQUIRED. ADD:
RULE 14 — TOMO never funds offers.
funder=tomo is forbidden. TOMO does not subsidize discounts; the funder
must be restaurant, partner, or a third-party loyalty program. Listings
with funder=tomo are auto-rejected at ingest.
RULE 15 — No fake savings_pct.
estimated_savings_pct must be computed from real pre_offer_total_inr ÷
post_offer_total_inr math. Inflating "30% off" when actual is 12% =
consumer-protection breach + listing rejection.
RULE 16 — No fake inventory_remaining.
inventory_limit must reflect actual contractual limit; inventory_remaining
must decrement honestly. Showing "Only 5 left!" when actual is 50 = breach.
RULE 17 — Voucher codes must be real, not gimmick.
voucher_code_required=true means the code actually changes the price.
Codes that do nothing but make the user feel like they got a deal = breach.
RULE 18 — Funder disclosure mandatory.
funder_disclosure_text must accurately tell user who paid. "EazyDiner-funded"
vs "Restaurant-funded" matters because it affects renewal economics.
RULE 19 — Loyalty membership cannot be silently required.
If loyalty_membership_required is set to anything other than 'none', the
search-result UI must surface this clearly. Hiding the requirement until
checkout = breach.
RULE 20 — Offers cannot be commission-shaped.
Best offer ranking by `savings_inr` + match. If TOMO detects ranking
correlated with partner commission rate, listing is suspended pending audit.
RULE 21 — Restaurant cannot refuse the offer at the door.
If a confirmed reservation says "30% off applied", the restaurant must
honor it. Refusing in-person citing fine print not in offer disclosure
= severe breach.
VERSION HISTORY
v1.0.0 — 2026-05-10 — Initial spec (delta over food.book_dine_in)