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

Birthday Add-On API Contract

This document describes the contract consumed by the Book-Bite birthday add-on screen.

Implementation target repository: hungryhub-team/book-bite.

Backend contract source: src/HHServer/superpowers/plans/2026-07-30-birthday-add-on-hh-server.md.

Transport

  • Base URL: APP_BASE_API_URL, normally https://partners-api.hungryhub.com/api/partner/v1/.
  • Authentication: Authorization: Bearer <partner-token>.
  • The request interceptor converts JavaScript camelCase to API snake_case and the response interceptor converts snake_case back to camelCase.
  • Create and update requests are multipart/form-data because image fields are uploaded files.

Endpoints

List

GET /birthday_add_ons

The selected restaurant request sends:

restaurant_id=<restaurant-id>
page=1
per_page=10
filter[id]=<restaurant-add-on-id>
filter[name]=<name>
filter[status]=active|inactive
filter[tag]=family_birthday|festive_birthday|romantic_birthday
filter[created_date_from]=YYYY-MM-DD
filter[created_date_to]=YYYY-MM-DD
filter[created_date]=YYYY-MM-DD
filter[updated_date_from]=YYYY-MM-DD
filter[updated_date_to]=YYYY-MM-DD
filter[updated_date]=YYYY-MM-DD
order_by[col]=name|price|tag|id|created_at|updated_at
order_by[sort_by]=asc|desc

The current screen sends restaurant_id, page, per_page, and only the populated filter[...] values. The selected restaurant must be accessible to the authenticated partner.

Response rows are flat attributes inside the restaurant group:

{
  "data": {
    "restaurant_id": 42,
    "restaurant_name": "Example Restaurant",
    "free": {
      "id": 61,
      "add_on_id": 99,
      "add_on_type": "birthday",
      "is_free_birthday_add_on": true,
      "item_type": "items_list",
      "price_cents": 0,
      "price_currency": "THB",
      "pricing_type": "per_item",
      "name_en": "Free Birthday",
      "cover_image": "https://cdn.example/birthday-cover.jpg",
      "birthday_add_on_items": []
    },
    "premium": [],
    "meta": {
      "premium": {
        "current_page": 1,
        "per_page": 10,
        "total_pages": 1,
        "total_count": 0
      }
    }
  }
}

is_free_birthday_add_on is the authoritative free-row indicator. Book-Bite keeps a price fallback only for compatibility with an older server during rollout.

Detail

GET /birthday_add_ons/:restaurant_add_on_id

The detail response is JSON:API:

{
  "data": {
    "id": "61",
    "type": "birthday_add_on",
    "attributes": {
      "restaurant_id": 42,
      "add_on_id": 99,
      "add_on_type": "birthday",
      "is_free_birthday_add_on": true,
      "item_type": "items_list",
      "price_cents": 0,
      "price_currency": "THB",
      "name_en": "Free Birthday",
      "description_en": "Birthday surprise",
      "birthday_add_on_items": [
        {
          "id": 1,
          "birthday_item": "cake",
          "display_name_en": "Cake",
          "description_en": "Birthday cake",
          "image": "https://cdn.example/cake.jpg",
          "index_number": 1
        }
      ],
      "add_on_menus": null
    }
  }
}

Payment type options

Book-Bite must use the existing partner payment-type endpoint instead of receiving the full payment catalog in every birthday add-on response:

GET /restaurant_packages/payment_types

Response:

{
  "data": [
    { "id": 1, "name": "credit_card" },
    { "id": 4, "name": "promptpay" }
  ]
}

The endpoint applies the restaurant-currency filtering on the server. The birthday add-on serializer returns only the selected payment_types rows needed to hydrate/edit the form; it does not return valid_payment_types or charge-internal fields.

Create

POST /birthday_add_ons
Content-Type: multipart/form-data

The server resolves the restaurant through default_restaurant: an explicit top-level restaurant_id is accepted when present and access-checked, while an omitted value falls back to the authenticated staff member’s default restaurant. For booking create/update, a nested reservation[restaurant_id]=null is also normalized to that default restaurant before the reservation service runs. Do not use a nested add-on restaurant field as request-level context.

Partner booking requests do not need to send dining_occasion_id. When a birthday add-on is selected without an occasion, hh-server automatically assigns the Birthday occasion before applying the shared birthday booking rules. Paid birthday add-ons may still be booked with another explicit occasion; an explicit occasion is preserved.

add_on[name_en]=Premium Birthday
add_on[name_th]=...
add_on[description_en]=Birthday surprise
add_on[description_th]=...
add_on[tag]=family_birthday
add_on[item_type]=items_list
add_on[price_currency]=THB
add_on[original_price_cents]=59900
add_on[limit_per_day]=0
add_on[minimum_booking_time_in_advance]=0
add_on[require_prepayment]=false
add_on[add_on_payment_types_attributes][0][payment_type_id]=<payment-type-id>
add_on[cover_image]=<image-file>
add_on[pricing_attributes][price_cents]=59900
add_on[pricing_attributes][price_currency]=THB
add_on[pricing_attributes][pricing_type]=per_item
add_on[birthday_add_on_items_attributes][0][birthday_item]=cake
add_on[birthday_add_on_items_attributes][0][display_name_en]=Cake
add_on[birthday_add_on_items_attributes][0][description_en]=Birthday cake
add_on[birthday_add_on_items_attributes][0][image]=<image-file>
add_on[birthday_add_on_items_attributes][0][index_number]=1

For item_type=items_image, omit birthday_add_on_items_attributes and send add_on_menus_attributes[index][image] plus index_number. A free add-on uses price_cents=0; only one live, public free birthday add-on is allowed per restaurant. A conflict returns HTTP 409 with code BIRTHDAY_FREE_ADDON_MAX_ONE_REACHED.

Payment settings and time-in-advance

These fields must use the same value contract as the Admin Add-On Settings screen:

Admin UI stateWire keyValue contract
requirePrepaymentadd_on[require_prepayment]Boolean. false by default.
selected payment typesadd_on[add_on_payment_types_attributes]Repeatable nested attributes; multiple payment types are allowed.
minBookingTimeInAdvanceadd_on[minimum_booking_time_in_advance]Integer minutes. 0 means Follow Restaurant Level.

Payment type IDs are integer IDs from GET /restaurant_packages/payment_types; the FE must not submit payment type names or gateway codes in place of IDs. When require_prepayment=true, the form must require at least one selected payment type before submitting. When it is false, the form must submit _destroy=true for existing payment-type junction rows so turning prepayment off also removes stale selections.

Create example with multiple payment types:

restaurant_id=42
add_on[require_prepayment]=true
add_on[add_on_payment_types_attributes][0][payment_type_id]=1
add_on[add_on_payment_types_attributes][1][payment_type_id]=4
add_on[minimum_booking_time_in_advance]=30

Edit example removing an existing type while retaining another:

add_on[add_on_payment_types_attributes][0][id]=17
add_on[add_on_payment_types_attributes][0][payment_type_id]=1
add_on[add_on_payment_types_attributes][1][id]=22
add_on[add_on_payment_types_attributes][1][_destroy]=true

The detail/create/update response exposes the selected junction rows. Options are fetched from the separate payment-type endpoint above:

{
  "require_prepayment": true,
  "minimum_booking_time_in_advance": 30,
  "payment_types": [
    { "id": 17, "payment_type_id": 1, "name": "Credit Card" }
  ]
}

payment_types[].id is the payment-type junction ID and must be preserved on edit; payment_types[].payment_type_id is the selected option ID. The FE should populate the multi-select from GET /restaurant_packages/payment_types, preserve selected junction IDs, and use _destroy for removed existing rows to avoid duplicate junction records.

The time-in-advance select must mirror Admin’s option builder: 0 (Follow Restaurant Level), then 15/30/45 minutes and hourly values with 15-minute increments. The normal select does not include the Admin-only 1000000 (Unlimited) sentinel unless the product explicitly enables that display mode. The backend resolves 0 to the restaurant’s dine_in_min_booking_time_in_advance on create; a non-zero value remains an add-on-specific override.

Update

PATCH /birthday_add_ons/:restaurant_add_on_id
Content-Type: multipart/form-data

The nested add_on fields use the same names as create. To update active state, send:

add_on[restaurant_add_ons_attributes][0][id]=61
add_on[restaurant_add_ons_attributes][0][active]=false

To delete an existing item or menu image, send its ID with _destroy=true:

add_on[birthday_add_on_items_attributes][0][id]=1
add_on[birthday_add_on_items_attributes][0][_destroy]=true

Delete

DELETE /birthday_add_ons/:restaurant_add_on_id

Successful response:

{ "success": true, "data": null }

Display-mode contract

  • item_type=items_list uses birthday_add_on_items in the partner response.
  • item_type=items_image uses add_on_menus in the partner response.
  • cover_image, item image, and menu image are full URLs. Book-Bite must render them directly and must not prepend another base URL.

Validation failures return HTTP 422 with code=VALIDATION_FAILED. Unauthenticated requests return 401; inaccessible restaurant or record lookups return 404.