Skip to content

Holiday search and availability

The holiday search lets a customer pick dates, party size, park and accommodation preferences and see live, priced availability. Every result carries a SCID — a GUID identifying one bookable option at one price — and the SCID is the handle every downstream checkout uses.

If search breaks, nothing can be booked. It is the top of the funnel for [[legacy-holiday-checkout]] and [[woocommerce-holiday-checkout]].

Elite Parks’ availability API is slow and can only answer one narrow question at a time, so the site cannot query it per page render. Instead a search is executed once, its fully-priced results are flattened into custom MySQL tables, and every subsequent page (results grid, carousels, the “book now” link, the checkout) reads from those tables by SCID. That is why a search result can go stale, and why the whole checkout is built around re-validating against EP before taking money.

All paths relative to wp-content/themes/lovat-parks/.

PathWhat it does
inc/eliteparks/booking.php:369POST /wp-json/booking/v1/api/ — the single fat endpoint. Behaviour is chosen by the method parameter.
inc/eliteparks/booking.php:1625getData() — the dispatcher. ~20 method branches.
inc/eliteparks/booking.php:375GET /wp-json/booking/v1/search/ → getBookingCarousel() — reads cached results for the on-page carousels.
blocks/holiday-search/block-holiday-search.phpThe search form block (renders partials/holiday-search.php).
blocks/holiday-search-results/The results grid, incl. no-results.php.
blocks/holiday-carousel/, blocks/offers-carousel/Price-led carousels fed by booking/v1/search/.
inc/cleanup.php:175cleanup_results_cron — prunes stale cached results.
methodPurpose
available_booking_datesWhich arrival days are valid, from the bookingseasons table. Drives the datepicker.
find_availabilityThe main search. Calls EP FindAvailability, writes booking_searches + booking_search_results.
find_availability_rangeFlexible-dates variant across a date range.
find_availability_single_gradeAvailability for one accommodation grade (used on grade/park pages).
get_available_entitiesEP GetAvailabilityEntities — the bookable entity list.
get_setupTriggers UpdateEPBookingCoreData() (see below).
sync_all_booking_gradesRebuilds the flat booking_grades table from the grades CPT.
flowchart TD
    A[Search form block] -->|POST method=find_availability| B[getData]
    B --> C[EP FindAvailability SOAP]
    C --> D[booking_grade_filter + filterBookingGradeInstance<br/>match EP grades to WP grades CPT]
    D --> E[(booking_searches row - one per search)]
    D --> F[(booking_search_results rows - one per SCID)]
    F --> G[Results grid / carousels]
    G -->|/holidays/checkout/?scid=GUID| H[Checkout]
  1. Seasons gate the datepicker. available_booking_dates reads the bookingseasons table (park_code, no_of_nights, booking_type, arrival_day) so the customer can only pick arrival days the park actually accepts.
  2. The search runs. find_availability builds a SOAP request and calls EP FindAvailability (booking.php:1852).
  3. Results are matched to content. EP returns grade codes; booking_grade_filter() (booking.php:3241) and filterBookingGradeInstance() (booking.php:839) join those to the grades CPT so each result gets a title, image gallery, berths, bedrooms, pet-friendly flag, hot tub, decking and a URL. Grade content is denormalised into a flat booking_grades table for speed, kept in sync on acf/save_post and save_post_grades (booking.php:3783).
  4. Everything is persisted. One row in {prefix}booking_searches (the query), many rows in {prefix}booking_search_results (the answers). Each result row gets a freshly generated GUID as its scid, plus url (grade page) and book_now_url ({checkout_page}?scid=…).
  5. Downstream reads are by SCID. The checkout never re-runs a search; it looks up one row by scid and reserves that exact option in EP.
TableContents
{prefix}booking_searchesOne row per search: guid, arrival_date, nights, park/grade filters, occupancy, accommodation & glamping type, pitch, size, amenities JSON, search_source_url. Created in booking.php:3653.
{prefix}booking_search_resultsOne row per bookable option. Schema at booking.php:3541. Key columns: scid, park_code, grade_code, arrival_date, no_of_nights, price, standard_price, discount_amount, special_offer_code, occupancy counts, accommodation_id, booking_type, search_id. Indexed on search_id, scid, arrival_date, park_code, grade_code.
{prefix}booking_gradesFlat projection of the grades CPT for join speed. Created booking.php:3712.
bookingseasonsArrival-day rules per park / nights / booking type. Note: no wp_ prefix — it is queried by bare name (booking.php:1671).
HookScheduleWhat it does
daily_booking_crondailyUpdateEPBookingCoreData() (booking.php:1335) — pulls EP setup data and refreshes parks, grades and lead sources.
untagBookingsFromEPevery 1 minuteCheckoutAPI::untag_bookings() — releases expired legacy holds. The comment above it says “every 5 minutes” and the repo README says hourly; the code registers the 1min schedule (booking.php:3043). Trust the code.
cleanup_results_cronhourly (configurable)Deletes booking_search_results rows older than 30 days in batches of 5,000. Settings in cleanup_search_results_settings, admin page + log in inc/cleanup.php.
  • ACF options: search_page, checkout_page, confirmation_page, portal_page — the URLs results and checkouts link to. Read via get_field(…, 'option').
  • Cleanup: option cleanup_search_results_settings — age_days (default 30), batch_size (default 5000), schedule (default hourly). Defaults are constants at inc/cleanup.php:10-13.
  • Table versioning: options booking_search_results_table_version and the check_booking_*_table functions run dbDelta / ALTER TABLE on init.
  • EP credentials — see [[elite-parks-integration]].
  • A SCID is a row, not a reservation. Nothing is held in EP until checkout tags it. Two customers can hold the same SCID until one of them reaches checkout first.
  • Search results go stale silently. A SCID from yesterday still resolves to a row with yesterday’s price. The checkout is the only thing that re-checks EP. Never trust booking_search_results.price as current.
  • Schema changes are applied by ad-hoc ALTER TABLE guards, not migrations (booking.php:3600-3630 checks for sofa_bed, hot_tub, decking, no_of_infants by DESCRIBE). Adding a column means adding another guard, and bumping $table_version.
  • bookingseasons has no table prefix. Multisite or a prefixed staging DB will not find it.
  • getData() is ~1,400 lines with 20 branches and no routing table. Adding a method is easy; finding the one you want is not. Search for $method == rather than reading top to bottom.
  • Cleanup is aggressive by design. A 30-day-old SCID is deleted, which turns an old bookmarked “book now” link into a hard failure rather than a stale price. That is deliberate.
  • The site also uses search_cleanup() at booking.php:3437 exposed as POST booking/v1/clean/ — a manual trigger for the same job.
  • New search behaviour belongs in a new method branch in getData(), matching the existing shape (read params → build XML → EPInterface::call() → parse → write rows → json_encode a status/data envelope). Returning a different envelope shape breaks the front-end JS.
  • If you add a field to the results grid, you must update three places: the CREATE TABLE in create_booking_search_results_table(), the ALTER TABLE guard for existing installs, and the $wpdb->insert() format array (booking.php:1189) — the format array is positional and a mismatch corrupts every column after it.
  • New grade content that must appear in results also needs adding to sync_booking_grade_to_flat_table() (booking.php:3783), or it will be missing for anyone hitting the flat table.
  • Verify with: run a search on the front end, then SELECT * FROM wp_booking_search_results ORDER BY id DESC LIMIT 5; and confirm the new column is populated and book_now_url still resolves.
  • Logs: wp-content/uploads/logs/strategiq-booking.log (write_to_booking_log), plus …-benchmark.log and …-tagging.log.
  • Deliberately not abstracted: the flat booking_grades table duplicates the grades CPT. It exists because joining postmeta for every result was the original performance problem. Do not replace it with meta queries.

There are no automated tests for search. The PHPUnit suite covers only inc/lovat-checkout/src (see phpunit.xml.dist), and the one adjacent test — tests/unit/Product/SearchResultMapperTest.php — covers mapping a booking_search_results row into a WooCommerce product, not the search itself.

Verification is manual: search on the front end, check row counts in both tables, and check strategiq-booking.log for errors.