T
TOMO
Developer Docs
BETA These docs are under partner review. Some features described are roadmap items, not yet shipped. Verify against your sandbox before relying on any contract.
● DRAFTv1.0.0food.book_dine_in_with_offer

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)