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
-
Generate Timestamp
const timestamp = Math.floor(Date.now() / 1000).toString(); -
Create Raw Signature String
{timestamp}\r\n{HTTP_METHOD}\r\n{PATH}\r\n\r\n{BODY} -
Generate HMAC Signature
const signature = CryptoJS.HmacSHA256(rawSignature, secretKey).toString(); -
Create Authorization Token
{API_KEY}:{TIMESTAMP}:{SIGNATURE} -
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) - Requiredrestaurant.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) - Requiredrestaurant.country_code- Country code (e.g., "TH")
Contact Information
restaurant.phone- Primary contact phone - Requiredrestaurant.delivery_phone- Delivery contact phonerestaurant.secondary_phone- Secondary contact phonerestaurant.website- Restaurant website URL
Location
restaurant.address_en- Full address (English) - Requiredrestaurant.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
trueifaccept_subsidize_voucher: true - Business Rule: Can't mix "accept booking without package" and "accept restaurant-subsidize voucher" at the same time
- Solution: Set to
falseif enabling subsidized vouchers, or vice versa
- ⚠️ IMPORTANT: Cannot be
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) - Requiredrestaurant.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
truewill 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
- ⚠️ Same requirement as
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 - Requiredrestaurant.owner.email- Owner email (unique) - Requiredrestaurant.owner.phone- Owner phone - Requiredrestaurant.owner.password- Owner login password - Requiredrestaurant.owner.managers_attributes- Array of manager objects
Manager Attributes (Optional)
Each manager in managers_attributes array:
email- Manager email - Requiredreceive_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_attributeswhenallow_order_now: trueorsupport_order_now: truewill 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
trueifallow_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, ensureallow_booking_without_package: false
- ⚠️ IMPORTANT: Cannot be
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
- Example:
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:
- For each weekday, an
InventoryTemplateGroupis created - For each time slot,
InventoryTemplaterecords are created in 15-minute intervals - The restaurant is linked to each weekday's template group
- This data is used by the booking system to determine available reservation times
Google Reserve Integration (Optional)
restaurant.google_reserve_name- Google Reserve restaurant namerestaurant.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 namerestaurant.google_reserve_postal_code- Postal coderestaurant.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- Usecuisine_tag_idsinsteadyour_name,your_phone,your_email- Useownerattributes insteadbooking_alert_email,booking_alert_phone- Usemanagers_attributesinsteadhow_did_you_know_about_hh- Tracked separatelygoogle_review_score,google_review_total- Usegoogle_review_attributesinsteadcustom_seat_options- Usecustom_seatsinsteadhashtags- Use tag associations insteadaccount_manager- Managed internallyhomepage_setting- Managed through admin panelcorkage_charge_thb- Usecorkage_chargeinsteadhas_parking- Useparkinginsteadyoutube_link- Userestaurant_external_attributes.videosinstead
🔧 Implementation Details
Core Components
-
Authentication Concern (
app/controllers/api/admin/concerns/authentication.rb)- HMAC-SHA256 signature validation
- Doorkeeper application integration
- Comprehensive error handling
-
Restaurant Controller (
app/controllers/api/admin/restaurants_controller.rb)- Complete restaurant onboarding endpoint
- Strong parameter validation
- Detailed error responses
- Handles nested associations and translations
-
Restaurant Service (
app/services/restaurant_service/create_from_api.rb)- Atomic database transactions
- Nested association creation
- Tag assignment integration
- Translation handling via Globalize
-
Tag Assignment
- Tags are assigned by IDs (not names)
- All tag IDs must exist in
restaurant_tagstable - Tags are categorized by
tag_type(Cuisine, DiningStyle, Facility, Location, Award)
Database Models & Associations
Main Models
Restaurant- Main restaurant record with Globalize translationsOwner- Restaurant owner (Devise authenticated)Manager(OtherEmail) - Restaurant managersRestaurantTag- Categorized tagsCity- Location referenceInventorySource- Booking system integration
Associated Models
GoogleReview- Google review ratingsOrderNow- Order now settingsRestaurantExternal- External content (chalkboard, videos)RestaurantVoucherSetting- Voucher settingsRestaurantInfo- Additional info (VR links, etc.)
Translation Support
The API uses Globalize gem for multilingual support:
- Supported locales:
:en,:th,:cn - All
_enand_thsuffixed fields are stored inrestaurant_translationstable - Automatic fallback to default locale if translation is missing
Request Processing Flow
- Authentication: HMAC signature validation
- Parameter Validation: Strong parameters filtering
- Transaction Start: Database transaction begins
- Restaurant Creation: Main restaurant record created
- Translation Creation: English and Thai translations created
- Owner Creation: Owner account created with password
- Manager Creation: Manager accounts created (if provided)
- Tag Assignment: Tags assigned by IDs
- Nested Associations: Additional associations created:
- Google Review
- Order Now settings
- Restaurant External
- Voucher Settings
- Restaurant Info
- Inventory Source
- Transaction Commit: All changes committed atomically
- 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
- Rails application with Doorkeeper configured
- Database with required tables:
restaurants,restaurant_translationsowners,other_emails(managers)restaurant_tags,citiesgoogle_reviews,order_nows,restaurant_externalsrestaurant_voucher_settings,restaurant_infosinventory_sources
- Create Doorkeeper application named "Restaurant Onboarding"
- 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
-
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
-
Validation Errors
- Check all required fields are present:
name_en,city_id,address_en,phone,start_date - Verify city_id exists in
citiestable - Ensure owner email is unique (not already used)
- Check date formats (YYYY-MM-DD)
- Ensure
start_dateis 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
- Check all required fields are present:
-
Order Now / Delivery Validation Errors
- ❌ Error: "Please setup delivery inventory to activate order now"
- Root Cause: Setting
allow_order_now: trueorsupport_order_now: truewithout delivery inventory configured - Solution:
- Set both
allow_order_now: falseandsupport_order_now: falsefor initial creation - Omit
order_now_attributessection entirely - Configure delivery inventory separately first
- Update restaurant later to enable order now features
- Set both
-
Booking Flow Validation Errors
- ❌ Error: "Booking flow is not included in the list"
- Root Cause: Invalid value for
booking_flowfield - 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
-
Tag Assignment Issues
- Tag IDs must exist in
restaurant_tagstable - 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'
- Cuisine tags:
- Empty tag arrays are acceptable
- Tag IDs must exist in
-
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';
-
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: trueandaccept_subsidize_voucher: truesimultaneously -
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 } }
-
-
Translation Issues
- At least one language (English or Thai) should have content
- Fields ending with
_engo to English translation - Fields ending with
_thgo to Thai translation - Fallback to default locale if translation is missing
-
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
-
Check Doorkeeper Application
# In Rails console Doorkeeper::Application.find_by(name: "Restaurant Onboarding") -
Verify City Exists
# In Rails console City.find(1) -
Check Tag IDs
# In Rails console RestaurantTag.where(id: [6, 14], tag_type: 'Cuisine') -
Test Translation Storage
# In Rails console restaurant = Restaurant.last restaurant.translations -
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
Related Documentation
- Doorkeeper OAuth2 setup
- Globalize translation configuration
- Restaurant model documentation
- Tag management guide
Support
For issues or questions:
- Check this documentation
- Review error messages carefully
- Verify database state
- Check Rails logs
- Contact development team
Ready for Production! 🎉