Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Restaurant Onboarding API - Complete Documentation

📋 Overview

A secure, production-ready API for creating restaurants with comprehensive features including HMAC authentication, nested associations, DRY tag assignment, and manager support.


🔐 Authentication

HMAC Authentication Setup

The API uses HMAC-SHA256 authentication with Doorkeeper integration.

Required Application:

  • Name: Restaurant Onboarding
  • API Key: Your Doorkeeper application UID
  • Secret: Your Doorkeeper application secret

Authentication Process

  1. Generate Timestamp

    const timestamp = Math.floor(Date.now() / 1000).toString();
    
  2. Create Raw Signature String

    {timestamp}\r\n{HTTP_METHOD}\r\n{PATH}\r\n\r\n{BODY}
    
  3. Generate HMAC Signature

    const signature = CryptoJS.HmacSHA256(rawSignature, secretKey).toString();
    
  4. Create Authorization Token

    {API_KEY}:{TIMESTAMP}:{SIGNATURE}
    
  5. Set Authorization Header

    Authorization: hmac {API_KEY}:{TIMESTAMP}:{SIGNATURE}
    

🛠 API Endpoint

Create Restaurant

Endpoint: POST /api/admin/restaurants

Headers:

Authorization: hmac <generated_token>
Content-Type: application/json

Complete Request Example

{
  "restaurant": {
    "name_en": "Amazing Thai Restaurant",
    "name_th": "ร้านอาหารไทยเจ๋ง",
    "short_name_en": "Amazing Thai",
    "short_name_th": "ร้านเจ๋ง",

    "phone": "+66812345678",
    "delivery_phone": "+66812345679",
    "secondary_phone": "+66812345680",
    "website": "https://amazingthai.com",

    "address_en": "123 Main Street, Bangkok",
    "address_th": "123 ถนนหลัก กรุงเทพฯ",
    "lat": 13.7563,
    "lng": 100.5018,

    "misc_en": "Welcome to Amazing Thai Restaurant! We serve authentic Thai cuisine with a modern twist.",
    "misc_th": "ยินดีต้อนรับสู่ร้านอาหารไทยเจ๋ง! เราเสิร์ฟอาหารไทยแท้ๆ ด้วยความทันสมัย",
    "food_details_en": "Our menu features traditional Thai dishes prepared with fresh ingredients.",
    "food_details_th": "เมนูของเราประกอบด้วยอาหารไทยดั้งเดิมที่ปรุงด้วยวัตถุดิบสด",
    "ambience_en": "Modern Thai restaurant with traditional decoration and comfortable seating.",
    "ambience_th": "ร้านอาหารไทยทันสมัยพร้อมการตกแต่งแบบดั้งเดิมและที่นั่งสบาย",

    "booking_condition_en": "Reservations recommended. Cancellation must be made 2 hours in advance.",
    "booking_condition_th": "แนะนำให้จองล่วงหน้า ยกเลิกการจองต้องแจ้งล่วงหน้า 2 ชั่วโมง",
    "small_note_en": "Kids menu available. Free WiFi.",
    "small_note_th": "มีเมนูสำหรับเด็ก WiFi ฟรี",
    "self_pickup_message_en": "Your order will be ready for pickup in 30 minutes.",
    "self_pickup_message_th": "ออเดอร์ของคุณจะพร้อมรับภายใน 30 นาที",
    "confirm_msg_en": "Thank you for your reservation! See you soon.",
    "confirm_msg_th": "ขอบคุณสำหรับการจอง! แล้วเจอกันนะครับ",
    "custom_text_en": "Best Thai food in Bangkok",
    "custom_text_th": "อาหารไทยที่อร่อยที่สุดในกรุงเทพ",
    "request_question_en": "Any special dietary requirements?",
    "request_question_th": "มีข้อกำหนดอาหารพิเศษหรือไม่?",
    "request_choice_en": "Vegetarian,Vegan,No Spicy,Halal",
    "request_choice_th": "มังสวิรัติ,มังเจ,ไม่เผ็ด,ฮาลาล",

    "country_code": "TH",
    "city_id": 1,

    "res_duration": 120,
    "days_in_advance": 30,
    "min_booking_time": 60,
    "largest_table": 12,
    "min_party_size": 1,
    "commision": 15.5,
    "time_in_advance_to_rectify": 120,
    "minimum_seat_allotment": 0,
    "custom_seats": "2,4,6,8,10,12",
    "hours": "11:00-22:00",

    "accept_kids": true,
    "accept_voucher": true,
    "allow_booking": true,
    "allow_booking_without_package": false,
    "booking_flow": "date_first",
    "instant_confirm": true,
    "accept_group_booking": true,
    "kids_definition": "Children under 12 years old",
    "covers_require_additional": 10,

    "start_date": "2024-01-01",
    "expiry_date": "2025-12-31",

    "delivery_note": "Order ID: %{order_id}, Customer: %{customer_name}",
    "corkage_charge": "100",
    "activate_auto_call_driver": false,
    "minute_before_delivery_time": 30,
    "call_driver_time_limit_duration": 15,
    "allow_order_now": false,
    "support_order_now": false,

    "flag": false,
    "parking": true,
    "promoted_by_hh": false,
    "active": false,
    "user_id": 1,
    "old_link": "https://www.hungryhub.com/ayce/hh/amazing-thai",

    "start_time_interval": "15min",
    "payment_provider": "omise",

    "imenu_pro_link": "https://menu.example.com/link1, https://menu.example.com/link2",

    "cuisine_tag_ids": [6, 14],
    "dining_style_tag_ids": [992],
    "facility_tag_ids": [824, 825],
    "location_tag_ids": [248, 999],
    "award_tag_ids": [],

    "owner": {
      "owner_name": "John Smith",
      "email": "john@amazingthai.com",
      "phone": "+66812345678",
      "password": "securepassword123",
      "managers_attributes": [
        {
          "email": "manager1@amazingthai.com",
          "receive_booking_email": true,
          "receive_review_email": true,
          "receive_report_email": false
        },
        {
          "email": "manager2@amazingthai.com",
          "receive_booking_email": false,
          "receive_review_email": true,
          "receive_report_email": true
        }
      ]
    },

    "google_review_attributes": {
      "rating": 4.5,
      "total_reviews": 150
    },

    "restaurant_external_attributes": {
      "chalkboard": "Welcome to our restaurant!",
      "videos": ["https://www.youtube.com/embed/z_Lpf7qkOno"]
    },

    "voucher_setting_attributes": {
      "accept_subsidize_voucher": true
    },

    "restaurant_info_attributes": {
      "my_mooban_vr_link": "https://new-vr.realsee.jp/example/link"
    },

    "google_reserve_name": "Amazing Thai Restaurant",
    "google_reserve_country_code": "TH",
    "google_reserve_state_code": "BKK",
    "google_reserve_city": "Bangkok",
    "google_reserve_postal_code": "10110",
    "google_reserve_street_address": "123 Main Street, Pathumwan",

    "inventory_source_attributes": {
      "inv_source": "hungryhub"
    },

    "inventory_group": {
      "monday": [
        {
          "first": {"value": "11:00"},
          "last": {"value": "14:00"},
          "seat": {"value": 25}
        },
        {
          "first": {"value": "17:00"},
          "last": {"value": "22:00"},
          "seat": {"value": 30}
        }
      ],
      "tuesday": [
        {
          "first": {"value": "11:00"},
          "last": {"value": "14:00"},
          "seat": {"value": 25}
        }
      ]
    }
  }
}

Success Response

{
  "status": "success",
  "message": "Restaurant created successfully",
  "data": {
    "restaurant": {
      "id": 123,
      "name": "Amazing Thai Restaurant",
      "short_name_en": "Amazing Thai",
      "short_name_th": "ร้านเจ๋ง",
      "city_id": 1,
      "owner_id": 456,
      "lat": 13.7563,
      "lng": 100.5018,
      "phone": "+66812345678",
      "delivery_phone": "+66812345679",
      "secondary_phone": "+66812345680",
      "website": "https://amazingthai.com",
      "active": false,
      "allow_booking": true,
      "booking_flow": "date_first",
      "instant_confirm": true,
      "accept_kids": true,
      "parking": true,
      "start_date": "2024-01-01",
      "expiry_date": "2025-12-31",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    },
    "owner": {
      "id": 456,
      "owner_name": "John Smith",
      "email": "john@amazingthai.com",
      "phone": "+66812345678",
      "managers": [
        {
          "id": 789,
          "email": "manager1@amazingthai.com",
          "receive_booking_email": true,
          "receive_review_email": true,
          "receive_report_email": false
        },
        {
          "id": 790,
          "email": "manager2@amazingthai.com",
          "receive_booking_email": false,
          "receive_review_email": true,
          "receive_report_email": true
        }
      ]
    },
    "tags_assigned": {
      "cuisine_tags": [6, 14],
      "dining_style_tags": [992],
      "facility_tags": [824, 825],
      "location_tags": [248, 999],
      "award_tags": []
    },
    "google_review": {
      "id": 101,
      "restaurant_id": 123,
      "rating": 4.5,
      "total_reviews": 150
    },
    "order_now": {
      "id": 202,
      "restaurant_id": 123,
      "call_driver_before_food_ready": 10,
      "cooking_time": 25
    },
    "restaurant_external": {
      "id": 303,
      "restaurant_id": 123,
      "chalkboard": "Welcome to our restaurant!",
      "videos": ["https://www.youtube.com/embed/z_Lpf7qkOno"]
    },
    "voucher_setting": {
      "id": 404,
      "restaurant_id": 123,
      "accept_subsidize_voucher": true
    },
    "restaurant_info": {
      "id": 505,
      "restaurant_id": 123,
      "my_mooban_vr_link": "https://new-vr.realsee.jp/example/link"
    },
    "inventory_source": {
      "id": 606,
      "restaurant_id": 123,
      "inv_source": "hungryhub"
    }
  }
}

Error Response

{
  "status": "error",
  "message": "Validation failed",
  "errors": [
    {
      "field": "restaurant.name_en",
      "message": "can't be blank"
    },
    {
      "field": "restaurant.city_id",
      "message": "can't be blank"
    },
    {
      "field": "restaurant.owner.email",
      "message": "has already been taken"
    },
    {
      "field": "restaurant.cuisine_tag_ids",
      "message": "contains invalid tag ID: 9999"
    }
  ]
}

🧪 Testing with Postman

Environment Variables

api_url: http://localhost:3300
app_id: your-doorkeeper-app-uid
app_secret: your-doorkeeper-app-secret

Pre-request Script

// Get environment variables
const appId = pm.environment.get("app_id");
const appSecret = pm.environment.get("app_secret");
const timestamp = Math.floor(Date.now() / 1000).toString();

// Get request details
const httpVerb = pm.request.method;
const path = pm.request.url.getPath();
const payload = pm.request.body.raw || "";

// Create raw signature string
const rawSignature = `${timestamp}\r\n${httpVerb}\r\n${path}\r\n\r\n${payload}`;

// Generate HMAC-SHA256 signature
const signature = CryptoJS.HmacSHA256(rawSignature, appSecret).toString();

// Create auth token
const authToken = `${appId}:${timestamp}:${signature}`;

// Set the Authorization header
pm.request.headers.add({
    key: "Authorization",
    value: `hmac ${authToken}`
});

console.log("Raw signature:", rawSignature);
console.log("Signature:", signature);
console.log("Auth token:", authToken);

🧪 Testing with Shell Script

Use the provided test script located at bin/api_scripts/test_restaurant_api.sh:

# Run the test script
chmod +x bin/api_scripts/test_restaurant_api.sh
./bin/api_scripts/test_restaurant_api.sh

The script includes:

#!/bin/bash

# Configuration
API_URL="http://localhost:3300/api/admin/restaurants"
API_KEY="your-doorkeeper-app-uid"
SECRET_KEY="your-doorkeeper-app-secret"

# Generate timestamp
TIMESTAMP=$(date +%s)

# Prepare request body
REQUEST_BODY='{
  "restaurant": {
    "name_en": "Test Restaurant via Script",
    "name_th": "ร้านทดสอบ",
    "short_name_en": "Test Restaurant",
    "short_name_th": "ร้านทดสอบ",
    "city_id": 1,
    "address_en": "123 Script Street, Bangkok",
    "address_th": "123 ถนนสคริปต์ กรุงเทพฯ",
    "phone": "+66812345678",
    "lat": 13.7563,
    "lng": 100.5018,
    "parking": true,
    "accept_kids": true,
    "allow_booking": true,
    "active": false,
    "start_date": "2024-01-01",
    "expiry_date": "2025-12-31",
    "res_duration": 120,
    "days_in_advance": 30,
    "min_booking_time": 60,
    "largest_table": 8,
    "min_party_size": 1,
    "hours": "11:00-22:00",
    "owner": {
      "owner_name": "Script Owner",
      "email": "script.owner@example.com",
      "phone": "+66812345678",
      "password": "scriptpassword123",
      "managers_attributes": [
        {
          "email": "script.manager@example.com",
          "receive_booking_email": true,
          "receive_review_email": true,
          "receive_report_email": false
        }
      ]
    },
    "cuisine_tag_ids": [6],
    "inventory_source_attributes": {
      "inv_source": "hungryhub"
    }
  }
}'

# Create raw signature
RAW_SIGNATURE="${TIMESTAMP}\r\nPOST\r\n/api/admin/restaurants\r\n\r\n${REQUEST_BODY}"

# Generate HMAC signature
SIGNATURE=$(echo -n -e "$RAW_SIGNATURE" | openssl dgst -sha256 -hmac "$SECRET_KEY" | sed 's/^.* //')

# Create auth token
AUTH_TOKEN="${API_KEY}:${TIMESTAMP}:${SIGNATURE}"

echo "Testing Restaurant API..."
echo "Timestamp: $TIMESTAMP"
echo "Auth Token: $AUTH_TOKEN"
echo "Making request..."

# Make API request
curl -X POST "$API_URL" \
  -H "Content-Type: application/json" \
  -H "Authorization: hmac $AUTH_TOKEN" \
  -d "$REQUEST_BODY" \
  -w "\nHTTP Status: %{http_code}\n" \
  | python3 -m json.tool

Make it executable and run:

chmod +x test_restaurant_api.sh
./test_restaurant_api.sh

📋 Field Reference

Restaurant Core Fields

Basic Information

  • restaurant.name_en - Restaurant name (English) - Required
  • restaurant.name_th - Restaurant name (Thai)
  • restaurant.short_name_en - Short name for display (English)
  • restaurant.short_name_th - Short name for display (Thai)
  • restaurant.city_id - City ID (must exist in database) - Required
  • restaurant.country_code - Country code (e.g., "TH")

Contact Information

  • restaurant.phone - Primary contact phone - Required
  • restaurant.delivery_phone - Delivery contact phone
  • restaurant.secondary_phone - Secondary contact phone
  • restaurant.website - Restaurant website URL

Location

  • restaurant.address_en - Full address (English) - Required
  • restaurant.address_th - Full address (Thai)
  • restaurant.lat - GPS latitude (decimal)
  • restaurant.lng - GPS longitude (decimal)

Descriptions & Details

  • restaurant.misc_en - Miscellaneous description (English)
  • restaurant.misc_th - Miscellaneous description (Thai)
  • restaurant.food_details_en - Food description (English)
  • restaurant.food_details_th - Food description (Thai)
  • restaurant.ambience_en - Ambience description (English)
  • restaurant.ambience_th - Ambience description (Thai)

Booking Conditions & Messages

  • restaurant.booking_condition_en - Booking conditions (English)
  • restaurant.booking_condition_th - Booking conditions (Thai)
  • restaurant.small_note_en - Small notes/reminders (English)
  • restaurant.small_note_th - Small notes/reminders (Thai)
  • restaurant.self_pickup_message_en - Self pickup message (English)
  • restaurant.self_pickup_message_th - Self pickup message (Thai)
  • restaurant.confirm_msg_en - Confirmation message (English)
  • restaurant.confirm_msg_th - Confirmation message (Thai)
  • restaurant.custom_text_en - Custom promotional text (English)
  • restaurant.custom_text_th - Custom promotional text (Thai)

Customer Request Options

  • restaurant.request_question_en - Request question (English)
  • restaurant.request_question_th - Request question (Thai)
  • restaurant.request_choice_en - Request choices comma-separated (English)
  • restaurant.request_choice_th - Request choices comma-separated (Thai)

Booking Configuration

Reservation Settings

  • restaurant.res_duration - Reservation duration in minutes (e.g., 120)
  • restaurant.days_in_advance - How many days in advance booking is allowed (e.g., 30)
  • restaurant.min_booking_time - Minimum booking time in advance in minutes (e.g., 60)
  • restaurant.largest_table - Maximum party size (e.g., 12)
  • restaurant.min_party_size - Minimum party size (e.g., 1)
  • restaurant.commision - Commission percentage (decimal, e.g., 15.5)
  • restaurant.time_in_advance_to_rectify - Time in minutes to rectify reservation (e.g., 120)
  • restaurant.minimum_seat_allotment - Minimum seat allotment (integer)
  • restaurant.custom_seats - Custom seat options comma-separated (e.g., "2,4,6,8,10,12")
  • restaurant.hours - Operating hours (e.g., "11:00-22:00")
  • restaurant.start_time_interval - Booking time interval (e.g., "15min", "30min")

Booking Behavior

  • restaurant.accept_kids - Accept children (boolean)
  • restaurant.accept_voucher - Accept vouchers (boolean)
  • restaurant.allow_booking - Allow booking (boolean)
  • restaurant.allow_booking_without_package - Allow booking without package (boolean)
    • ⚠️ IMPORTANT: Cannot be true if accept_subsidize_voucher: true
    • Business Rule: Can't mix "accept booking without package" and "accept restaurant-subsidize voucher" at the same time
    • Solution: Set to false if enabling subsidized vouchers, or vice versa
  • restaurant.booking_flow - Booking flow type IMPORTANT: Valid values are ONLY:
    • "date_first" - Customer selects date first (default)
    • "package_first" - Customer selects package first
    • ⚠️ Any other value will cause validation error: "Booking flow is not included in the list"
  • restaurant.instant_confirm - Instant confirmation (boolean)
  • restaurant.accept_group_booking - Accept group bookings (boolean)
  • restaurant.kids_definition - Definition of kids (string, e.g., "Children under 12 years old")
  • restaurant.covers_require_additional - Number of covers requiring additional handling (integer)

Operational Settings

Dates & Status

  • restaurant.start_date - Restaurant start date (YYYY-MM-DD) - Required
  • restaurant.expiry_date - Restaurant expiry date (YYYY-MM-DD)
  • restaurant.active - Restaurant active status (boolean)
  • restaurant.flag - Flag status (boolean)

Delivery Settings

  • restaurant.delivery_note - Delivery note template (supports placeholders like %{order_id}, %{customer_name})
  • restaurant.activate_auto_call_driver - Auto call driver (boolean)
  • restaurant.minute_before_delivery_time - Minutes before delivery time (integer)
  • restaurant.call_driver_time_limit_duration - Call driver time limit in minutes (integer)
  • restaurant.allow_order_now - Allow order now feature (boolean)
    • ⚠️ IMPORTANT: Cannot be enabled without delivery inventory setup
    • Setting to true will cause validation error: "Please setup delivery inventory to activate order now"
    • Must configure delivery inventory first before enabling this feature
  • restaurant.support_order_now - Support order now (boolean)
    • ⚠️ Same requirement as allow_order_now - needs delivery inventory setup

Additional Features

  • restaurant.corkage_charge - Corkage charge amount (string)
  • restaurant.parking - Has parking (boolean)
  • restaurant.promoted_by_hh - Promoted by HungryHub (boolean)
  • restaurant.user_id - Associated user ID (integer)
  • restaurant.old_link - Old website link for migration (string)
  • restaurant.payment_provider - Payment provider (e.g., "omise", "stripe")
  • restaurant.imenu_pro_link - iMenu Pro links comma-separated (string)

Tag Associations

Tags are assigned by IDs from the restaurant_tags table. Each tag type has a specific tag_type value in the database.

  • restaurant.cuisine_tag_ids - Cuisine tags (array of integers, tag_type: "Cuisine")
  • restaurant.dining_style_tag_ids - Dining style tags (array of integers, tag_type: "DiningStyle")
  • restaurant.facility_tag_ids - Facility tags (array of integers, tag_type: "Facility")
  • restaurant.location_tag_ids - Location/neighborhood tags (array of integers, tag_type: "Location")
  • restaurant.award_tag_ids - Award tags (array of integers, tag_type: "Award")

Nested Associations

Owner Attributes (Required)

  • restaurant.owner.owner_name - Owner full name - Required
  • restaurant.owner.email - Owner email (unique) - Required
  • restaurant.owner.phone - Owner phone - Required
  • restaurant.owner.password - Owner login password - Required
  • restaurant.owner.managers_attributes - Array of manager objects

Manager Attributes (Optional)

Each manager in managers_attributes array:

  • email - Manager email - Required
  • receive_booking_email - Receive booking notifications (boolean)
  • receive_review_email - Receive review notifications (boolean)
  • receive_report_email - Receive report notifications (boolean)

Google Review Attributes (Optional)

  • restaurant.google_review_attributes.rating - Google review rating (float, e.g., 4.5)
  • restaurant.google_review_attributes.total_reviews - Total number of reviews (integer)

Order Now Attributes (Optional)

⚠️ IMPORTANT: This section should be OMITTED unless delivery inventory is already configured.

  • restaurant.order_now_attributes.call_driver_before_food_ready - Minutes to call driver before food ready (integer)
  • restaurant.order_now_attributes.cooking_time - Cooking time in minutes (integer)

Validation Requirements:

  • Including order_now_attributes when allow_order_now: true or support_order_now: true will cause validation error
  • Error message: "Please setup delivery inventory to activate order now"
  • Recommendation: Omit this entire section for initial restaurant creation
  • Configure delivery inventory first, then update restaurant to enable order now features

Restaurant External Attributes (Optional)

  • restaurant.restaurant_external_attributes.chalkboard - Chalkboard message (text)
  • restaurant.restaurant_external_attributes.videos - Array of video URLs (array of strings)

Voucher Setting Attributes (Optional)

  • restaurant.voucher_setting_attributes.accept_subsidize_voucher - Accept subsidized vouchers (boolean)
    • ⚠️ IMPORTANT: Cannot be true if allow_booking_without_package: true
    • Business Rule: Can't mix "accept booking without package" and "accept restaurant-subsidize voucher" at the same time
    • Solution: If set to true, ensure allow_booking_without_package: false

Restaurant Info Attributes (Optional)

  • restaurant.restaurant_info_attributes.my_mooban_vr_link - VR tour link (string)

Inventory Source Attributes (Optional)

  • restaurant.inventory_source_attributes.inv_source - Inventory source identifier (string)
    • Example: "hungryhub", "third_party"
    • Default: "hungryhub" if not provided

Inventory Group (Optional)

Weekly time slot schedule for restaurant availability. Creates inventory template groups and templates for each weekday.

  • restaurant.inventory_group - JSON object with weekday keys and time slot arrays
    • Format: Each weekday contains an array of time slot objects
    • Time Slot Structure:
      • first.value - Start time in HH:MM format (e.g., "11:00")
      • last.value - End time in HH:MM format (e.g., "14:00")
      • seat.value - Number of available seats/quantity (integer)
    • Weekday Keys: monday, tuesday, wednesday, thursday, friday, saturday, sunday
    • Note: The system automatically creates 15-minute interval templates between start and end times

Example:

{
  "inventory_group": {
    "monday": [
      {
        "first": {"value": "11:00"},
        "last": {"value": "14:00"},
        "seat": {"value": 25}
      },
      {
        "first": {"value": "17:00"},
        "last": {"value": "22:00"},
        "seat": {"value": 30}
      }
    ],
    "tuesday": [
      {
        "first": {"value": "11:00"},
        "last": {"value": "14:00"},
        "seat": {"value": 25}
      }
    ]
  }
}

How it works:

  1. For each weekday, an InventoryTemplateGroup is created
  2. For each time slot, InventoryTemplate records are created in 15-minute intervals
  3. The restaurant is linked to each weekday's template group
  4. This data is used by the booking system to determine available reservation times

Google Reserve Integration (Optional)

  • restaurant.google_reserve_name - Google Reserve restaurant name
  • restaurant.google_reserve_country_code - Country code (e.g., "TH")
  • restaurant.google_reserve_state_code - State/Province code (e.g., "BKK")
  • restaurant.google_reserve_city - City name
  • restaurant.google_reserve_postal_code - Postal code
  • restaurant.google_reserve_street_address - Street address

Inventory Source Attributes (Optional)

  • restaurant.inventory_source_attributes.inv_source - Inventory source identifier (string)
    • Valid values: "hungryhub", "chope", "tablein", or other inventory system identifiers

    • Example:

      "inventory_source_attributes": {
        "inv_source": "hungryhub"
      }
      
    • This creates or associates the restaurant with an inventory source for menu management

Field Notes

Translation Fields

All fields ending with _en or _th are stored in the restaurant_translations table using the Globalize gem. The API automatically handles creating translations for both English (:en) and Thai (:th) locales.

Deprecated/Not Used

The following fields from legacy systems are not included in this API:

  • cuisine_id - Use cuisine_tag_ids instead
  • your_name, your_phone, your_email - Use owner attributes instead
  • booking_alert_email, booking_alert_phone - Use managers_attributes instead
  • how_did_you_know_about_hh - Tracked separately
  • google_review_score, google_review_total - Use google_review_attributes instead
  • custom_seat_options - Use custom_seats instead
  • hashtags - Use tag associations instead
  • account_manager - Managed internally
  • homepage_setting - Managed through admin panel
  • corkage_charge_thb - Use corkage_charge instead
  • has_parking - Use parking instead
  • youtube_link - Use restaurant_external_attributes.videos instead

🔧 Implementation Details

Core Components

  1. Authentication Concern (app/controllers/api/admin/concerns/authentication.rb)

    • HMAC-SHA256 signature validation
    • Doorkeeper application integration
    • Comprehensive error handling
  2. Restaurant Controller (app/controllers/api/admin/restaurants_controller.rb)

    • Complete restaurant onboarding endpoint
    • Strong parameter validation
    • Detailed error responses
    • Handles nested associations and translations
  3. Restaurant Service (app/services/restaurant_service/create_from_api.rb)

    • Atomic database transactions
    • Nested association creation
    • Tag assignment integration
    • Translation handling via Globalize
  4. Tag Assignment

    • Tags are assigned by IDs (not names)
    • All tag IDs must exist in restaurant_tags table
    • Tags are categorized by tag_type (Cuisine, DiningStyle, Facility, Location, Award)

Database Models & Associations

Main Models

  • Restaurant - Main restaurant record with Globalize translations
  • Owner - Restaurant owner (Devise authenticated)
  • Manager (OtherEmail) - Restaurant managers
  • RestaurantTag - Categorized tags
  • City - Location reference
  • InventorySource - Booking system integration

Associated Models

  • GoogleReview - Google review ratings
  • OrderNow - Order now settings
  • RestaurantExternal - External content (chalkboard, videos)
  • RestaurantVoucherSetting - Voucher settings
  • RestaurantInfo - Additional info (VR links, etc.)

Translation Support

The API uses Globalize gem for multilingual support:

  • Supported locales: :en, :th, :cn
  • All _en and _th suffixed fields are stored in restaurant_translations table
  • Automatic fallback to default locale if translation is missing

Request Processing Flow

  1. Authentication: HMAC signature validation
  2. Parameter Validation: Strong parameters filtering
  3. Transaction Start: Database transaction begins
  4. Restaurant Creation: Main restaurant record created
  5. Translation Creation: English and Thai translations created
  6. Owner Creation: Owner account created with password
  7. Manager Creation: Manager accounts created (if provided)
  8. Tag Assignment: Tags assigned by IDs
  9. Nested Associations: Additional associations created:
    • Google Review
    • Order Now settings
    • Restaurant External
    • Voucher Settings
    • Restaurant Info
    • Inventory Source
  10. Transaction Commit: All changes committed atomically
  11. Response: Success response with all created records

Error Handling

The API provides detailed error responses for various scenarios:

Validation Errors (422)

  • Missing required fields
  • Invalid data formats
  • Unique constraint violations
  • Foreign key violations (invalid IDs)

Authentication Errors (401)

  • Invalid HMAC signature
  • Missing authorization header
  • Expired timestamp
  • Unknown Doorkeeper application

Business Logic Errors (422)

  • Duplicate owner email
  • Invalid tag IDs
  • Invalid city_id
  • Date validation failures

Test Coverage

  • ✅ Authentication concern unit tests
  • ✅ Controller integration tests
  • ✅ Service object tests
  • ✅ Factory definitions
  • ✅ Shared test contexts
  • ✅ Translation handling tests
  • ✅ Nested association tests

🚀 Production Deployment

Prerequisites

  1. Rails application with Doorkeeper configured
  2. Database with required tables:
    • restaurants, restaurant_translations
    • owners, other_emails (managers)
    • restaurant_tags, cities
    • google_reviews, order_nows, restaurant_externals
    • restaurant_voucher_settings, restaurant_infos
    • inventory_sources
  3. Create Doorkeeper application named "Restaurant Onboarding"
  4. Globalize gem configured for translations

Security Considerations

  • HMAC signatures prevent replay attacks (timestamp validation)
  • Strong parameter filtering prevents mass assignment
  • Database transactions ensure data consistency
  • Comprehensive input validation
  • Password encryption via Devise
  • Authorization via Pundit (if implemented)

Monitoring & Logging

  • ElasticAPM integration for performance monitoring
  • BUSINESS_LOGGER for business context logging
  • Comprehensive error tracking
  • Request/response logging

Performance Considerations

  • Database transactions for atomicity
  • Eager loading for associations
  • Index optimization on frequently queried fields
  • Background job processing for heavy operations (if needed)

🆘 Troubleshooting

Common Issues

  1. Authentication Fails

    • Verify Doorkeeper application exists with correct name "Restaurant Onboarding"
    • Check timestamp is within acceptable range (not too old/future)
    • Ensure signature generation matches expected format
    • Verify no extra whitespace in authorization header
  2. Validation Errors

    • Check all required fields are present: name_en, city_id, address_en, phone, start_date
    • Verify city_id exists in cities table
    • Ensure owner email is unique (not already used)
    • Check date formats (YYYY-MM-DD)
    • Ensure start_date is not in the past
    • booking_flow must be either "date_first" or "package_first" - any other value will fail
    • Cannot enable order_now without delivery inventory - see section below
  3. Order Now / Delivery Validation Errors

    • ❌ Error: "Please setup delivery inventory to activate order now"
    • Root Cause: Setting allow_order_now: true or support_order_now: true without delivery inventory configured
    • Solution:
      • Set both allow_order_now: false and support_order_now: false for initial creation
      • Omit order_now_attributes section entirely
      • Configure delivery inventory separately first
      • Update restaurant later to enable order now features
  4. Booking Flow Validation Errors

    • ❌ Error: "Booking flow is not included in the list"
    • Root Cause: Invalid value for booking_flow field
    • Valid Values Only:
      • "date_first" - Customer selects date first (recommended default)
      • "package_first" - Customer selects package first
    • Solution: Use one of the two valid values above
  5. Tag Assignment Issues

    • Tag IDs must exist in restaurant_tags table
    • Verify tag_type matches the intended category:
      • Cuisine tags: tag_type = 'Cuisine'
      • Dining style tags: tag_type = 'DiningStyle'
      • Facility tags: tag_type = 'Facility'
      • Location tags: tag_type = 'Location'
      • Award tags: tag_type = 'Award'
    • Empty tag arrays are acceptable
  6. Duplicate Email Errors

    • ❌ Error: "Email has already been taken"
    • Root Cause: Owner email already exists in database
    • Solution: Use a unique email address for the owner
    • Check existing owners: SELECT email FROM owners WHERE email = 'your-email@example.com';
  7. Voucher and Booking Package Conflict

    • ❌ Error: "Can't mix 'accept booking without package' and 'accept restaurant-subsidize voucher' at the same time"

    • Root Cause: Business rule prevents enabling both allow_booking_without_package: true and accept_subsidize_voucher: true simultaneously

    • Solution: Choose one of the following configurations:

      // Option 1: Allow booking without package (NO subsidized vouchers)
      {
        "allow_booking_without_package": true,
        "voucher_setting_attributes": {
          "accept_subsidize_voucher": false
        }
      }
      
      // Option 2: Accept subsidized vouchers (booking REQUIRES package)
      {
        "allow_booking_without_package": false,
        "voucher_setting_attributes": {
          "accept_subsidize_voucher": true
        }
      }
      
  8. Translation Issues

    • At least one language (English or Thai) should have content
    • Fields ending with _en go to English translation
    • Fields ending with _th go to Thai translation
    • Fallback to default locale if translation is missing
  9. Nested Association Errors

    • Owner attributes are required
    • Manager emails must be unique per restaurant
    • Invalid nested attributes will rollback entire transaction
    • Check for proper JSON structure in nested objects

Error Codes

  • 401 - Authentication failed (HMAC signature invalid)
  • 400 - Bad request (missing required parameters, malformed JSON)
  • 422 - Validation failed (business logic errors, invalid data)
  • 500 - Internal server error (unexpected errors, database issues)

Debugging Tips

  1. Check Doorkeeper Application

    # In Rails console
    Doorkeeper::Application.find_by(name: "Restaurant Onboarding")
    
  2. Verify City Exists

    # In Rails console
    City.find(1)
    
  3. Check Tag IDs

    # In Rails console
    RestaurantTag.where(id: [6, 14], tag_type: 'Cuisine')
    
  4. Test Translation Storage

    # In Rails console
    restaurant = Restaurant.last
    restaurant.translations
    
  5. Verify Owner Creation

    # In Rails console
    Owner.find_by(email: "john@amazingthai.com")
    

SQL Queries for Verification

-- Check if city exists
SELECT * FROM cities WHERE id = 1;

-- Check tag IDs and types
SELECT id, name, tag_type FROM restaurant_tags WHERE id IN (6, 14);

-- Verify restaurant and translations
SELECT r.id, r.name, rt.locale, rt.name, rt.address 
FROM restaurants r 
LEFT JOIN restaurant_translations rt ON r.id = rt.restaurant_id 
WHERE r.id = 123;

-- Check owner and managers
SELECT o.id, o.email, oe.email as manager_email 
FROM owners o 
LEFT JOIN other_emails oe ON o.id = oe.owner_id 
WHERE o.id = 456;

📝 Change Log

Version 1.0.0

  • ✅ Initial HMAC authentication implementation
  • ✅ Complete restaurant creation with nested associations
  • ✅ Comprehensive field support (60+ fields)
  • ✅ Tag assignment by IDs with type validation
  • ✅ Globalize translation support (EN, TH)
  • ✅ Manager email support with notification preferences
  • ✅ Google Review integration
  • ✅ Order Now settings
  • ✅ Restaurant External (chalkboard, videos)
  • ✅ Voucher settings
  • ✅ Restaurant Info (VR links)
  • ✅ Inventory Source integration
  • ✅ Google Reserve integration fields
  • ✅ Comprehensive error handling
  • ✅ Production-ready test coverage
  • ✅ Complete documentation with examples

📚 Additional Resources

API Testing Tools

  • Postman: Recommended for manual testing
  • cURL: For command-line testing
  • Shell Script: Provided in bin/api_scripts/test_restaurant_api.sh
  • Doorkeeper OAuth2 setup
  • Globalize translation configuration
  • Restaurant model documentation
  • Tag management guide

Support

For issues or questions:

  1. Check this documentation
  2. Review error messages carefully
  3. Verify database state
  4. Check Rails logs
  5. Contact development team

Ready for Production! 🎉