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]].
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”All paths relative to wp-content/themes/lovat-parks/.
| Path | What it does |
|---|---|
inc/eliteparks/booking.php:369 | POST /wp-json/booking/v1/api/ — the single fat endpoint. Behaviour is chosen by the method parameter. |
inc/eliteparks/booking.php:1625 | getData() — the dispatcher. ~20 method branches. |
inc/eliteparks/booking.php:375 | GET /wp-json/booking/v1/search/ → getBookingCarousel() — reads cached results for the on-page carousels. |
blocks/holiday-search/block-holiday-search.php | The 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:175 | cleanup_results_cron — prunes stale cached results. |
method values that matter
Section titled “method values that matter”method | Purpose |
|---|---|
available_booking_dates | Which arrival days are valid, from the bookingseasons table. Drives the datepicker. |
find_availability | The main search. Calls EP FindAvailability, writes booking_searches + booking_search_results. |
find_availability_range | Flexible-dates variant across a date range. |
find_availability_single_grade | Availability for one accommodation grade (used on grade/park pages). |
get_available_entities | EP GetAvailabilityEntities — the bookable entity list. |
get_setup | Triggers UpdateEPBookingCoreData() (see below). |
sync_all_booking_grades | Rebuilds the flat booking_grades table from the grades CPT. |
How it works
Section titled “How it works”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]
- Seasons gate the datepicker.
available_booking_datesreads thebookingseasonstable (park_code, no_of_nights, booking_type, arrival_day) so the customer can only pick arrival days the park actually accepts. - The search runs.
find_availabilitybuilds a SOAP request and calls EPFindAvailability(booking.php:1852). - Results are matched to content. EP returns grade codes;
booking_grade_filter()(booking.php:3241) andfilterBookingGradeInstance()(booking.php:839) join those to thegradesCPT 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 flatbooking_gradestable for speed, kept in sync onacf/save_postandsave_post_grades(booking.php:3783). - 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 itsscid, plusurl(grade page) andbook_now_url({checkout_page}?scid=…). - Downstream reads are by SCID. The checkout never re-runs a search; it looks up one row by
scidand reserves that exact option in EP.
Tables
Section titled “Tables”| Table | Contents |
|---|---|
{prefix}booking_searches | One 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_results | One 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_grades | Flat projection of the grades CPT for join speed. Created booking.php:3712. |
bookingseasons | Arrival-day rules per park / nights / booking type. Note: no wp_ prefix — it is queried by bare name (booking.php:1671). |
Scheduled jobs
Section titled “Scheduled jobs”| Hook | Schedule | What it does |
|---|---|---|
daily_booking_cron | daily | UpdateEPBookingCoreData() (booking.php:1335) — pulls EP setup data and refreshes parks, grades and lead sources. |
untagBookingsFromEP | every 1 minute | CheckoutAPI::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_cron | hourly (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. |
Configuration
Section titled “Configuration”- ACF options:
search_page,checkout_page,confirmation_page,portal_page— the URLs results and checkouts link to. Read viaget_field(…, 'option'). - Cleanup: option
cleanup_search_results_settings—age_days(default 30),batch_size(default 5000),schedule(default hourly). Defaults are constants atinc/cleanup.php:10-13. - Table versioning: options
booking_search_results_table_versionand thecheck_booking_*_tablefunctions rundbDelta/ALTER TABLEoninit. - EP credentials — see [[elite-parks-integration]].
Invariants and gotchas
Section titled “Invariants and gotchas”- 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.priceas current. - Schema changes are applied by ad-hoc
ALTER TABLEguards, not migrations (booking.php:3600-3630checks forsofa_bed,hot_tub,decking,no_of_infantsbyDESCRIBE). Adding a column means adding another guard, and bumping$table_version. bookingseasonshas 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 amethodis 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()atbooking.php:3437exposed asPOST booking/v1/clean/— a manual trigger for the same job.
Changing it safely
Section titled “Changing it safely”- New search behaviour belongs in a new
methodbranch ingetData(), matching the existing shape (read params → build XML →EPInterface::call()→ parse → write rows →json_encodeastatus/dataenvelope). 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 TABLEincreate_booking_search_results_table(), theALTER TABLEguard 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 andbook_now_urlstill resolves. - Logs:
wp-content/uploads/logs/strategiq-booking.log(write_to_booking_log), plus…-benchmark.logand…-tagging.log. - Deliberately not abstracted: the flat
booking_gradestable duplicates thegradesCPT. 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.