Skip to content

Legacy holiday checkout

The original single-holiday checkout: a bespoke Vue-driven Gutenberg block at /holidays/checkout/?scid=…, backed by the booking custom post type and Opayo (Elavon) as the payment gateway.

It has been superseded by [[woocommerce-holiday-checkout]] but is still live code. It is the rollback path, and it still owns every historic booking record and the admin tooling built around them.

It was the site’s only checkout until the 2026 migration. It is kept in place — not deleted — for two reasons:

  1. Rollback. Turning off wc_checkout_enabled must restore a working checkout instantly. That only works if this code path stays untouched.
  2. History. Every booking taken before the migration is a booking post. The ownership portal, issues dashboard, booking activity log and DotDigital sync all read that CPT.

Treat it as frozen. Changes here should be bug fixes only.

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

PathWhat it does
blocks/holiday-checkout/block-holiday-checkout.phpThe checkout block. Runs on the page whose slug is checkout, reads ?scid=. Only reached when the WooCommerce flag is off — [[woocommerce-holiday-checkout]]‘s ScidRouter intercepts first at template_redirect priority 1.
blocks/holiday-confirmation/block-holiday-confirmation.phpThank-you page. Confirms the payment, renders the receipt, pushes to DotDigital.
inc/eliteparks/checkoutAPI.php:7CheckoutAPI — the ~2,500-line class that does everything.
inc/eliteparks/checkoutBooking.php:5CheckoutBooking — builds the initial page payload for Vue from cached search data.
inc/eliteparks/booking.php:1625getData() — the same fat REST dispatcher search uses; the checkout’s AJAX steps are method branches on it (update_extras, apply_promo_code, remove_promo_code, checkout_proceed_to_payment, get_failed_booking, …).
inc/opayo/OpayoApi.phpOpayo/Elavon integration. OpayoApi-local.php is the local-dev variant.
flowchart TD
    A[/holidays/checkout/?scid=X/] --> B[CheckoutAPI::is_confirmed / is_untagged]
    B --> C[tag_local_scid]
    C --> D[create booking CPT post, status=created]
    D --> E[EP CreateBooking + UpdateBookingAvailability + TagBooking]
    E --> F[Vue multi-step form: details, extras, promo, prefs]
    F --> G[Opayo card tokenisation]
    G -->|3DS challenge| H[?3dsecure=true → process_3dsecure_response]
    G -->|no challenge| I[make_payment]
    H --> I
    I --> J[EP MakePayment + ConfirmBooking]
    J --> K[confirmation page → confirm_payment → push_to_dotdigital]

CheckoutAPI::tag_local_scid() (checkoutAPI.php:270) is the first thing the block calls. It:

  1. Looks up the SCID in the search results (see [[holiday-search-and-availability]]).
  2. Creates a booking post if one does not already exist for that SCID (create_local_booking, post title = the SCID).
  3. Writes scid back onto the booking and the booking id onto the search result — into the booking_search_results.booking_id column when enable_v2_search is on, or ACF meta on the result post when it is not.
  4. Seeds booking_status = created.

booking_status drives the whole lifecycle: created → pending_payment → confirmed, with booking_tag_date / booking_untag_date recording the hold.

Opayo runs client-side (sagepay.js, live or sandbox chosen by the SAGEPAY_IS_PRODUCTION constant) to tokenise the card, so PANs never reach the server. A 3DS challenge returns the customer to the same URL with ?3dsecure=true, where process_3dsecure_response() (checkoutAPI.php:2541) resolves it and redirects to the confirmation page with ?payment_id=…, or back to checkout with ?payment_error=1.

Opayo config is per park: opayo_config($key, $park_code) reads the OPAYO_CONFIG constant, keyed by park code (checkoutAPI.php:2057). Different parks settle to different merchant accounts.

The untagBookingsFromEP cron (registered booking.php:3043, every 1 minute) calls CheckoutAPI::untag_bookings(), which finds booking posts in created or pending_payment with no booking_untag_date, and untags any whose booking_tag_date is more than 20 minutes old.

It re-reads booking_status straight from the database rather than trusting the WP_Query result, and skips anything already confirmed. That guard exists because object-cache drift previously let the cron untag a booking a customer had just paid for — a double-booking. Do not remove it (checkoutAPI.php:2120-2135).

The confirmation block calls confirm_payment($payment_id), then get_confirmed_booking_data(), then push_to_dotdigital() (checkoutAPI.php:1915). Adding ?debug=1 renders the page without confirming or pushing.

SettingWherePurpose
SAGEPAY_IS_PRODUCTIONwp-config.phpLive vs sandbox Opayo JS.
OPAYO_CONFIGwp-config.phpPer-park Opayo credentials array, keyed by park code. Names only — never record values.
checkout_page, confirmation_page, search_page, portal_pageACF optionsWhere the flow redirects.
enable_v2_searchACF optionSwitches the booking↔search-result link between the flat table and ACF meta.
wc_checkout_enabled / LP_USE_WOOCOMMERCE_CHECKOUTACF option / constantWhen true this whole flow is bypassed. See [[woocommerce-holiday-checkout]].
EP credentialswp-config.phpSee [[elite-parks-integration]].
  • This is the rollback path. It must keep working. Any change to shared code (booking.php, checkoutAPI.php, the search tables) has to be tested with the WooCommerce flag off as well as on.
  • The untag cron’s DB re-read is a double-booking guard, not defensive noise.
  • CheckoutAPI is stateful. new CheckoutAPI($bookingID) binds to one booking; new CheckoutAPI() resolves it from $_GET['scid']. The same class is used from REST handlers, blocks and cron, so constructor arguments matter.
  • Opayo credentials are per park. A booking spanning the wrong park code silently uses the wrong merchant account.
  • update_field() on a booking is used as the transaction log. There is no separate state machine — booking_status, booking_tag_date, booking_untag_date and previous_part_payments_made are ACF fields, and out-of-order writes corrupt the lifecycle.
  • previous_part_payments_made is a comma-joined string, and duplicate-payment protection is an in_array(explode(',')) check. It is also read by [[booking-portal-balance-payments]].
  • There are no tests. None of this path is covered by PHPUnit.
  • Logs: wp-content/uploads/logs/strategiq-checkout.log, strategiq-booking.log, strategiq-tagging.log.
  • Prefer fixing forward in [[woocommerce-holiday-checkout]]. Only patch here when the bug also affects the rollback path or historic data.
  • Checkout AJAX steps are method branches in getData() (booking.php:1625) — match the existing json_encode(['status' => …]) envelope or the Vue app breaks.
  • After changing anything in CheckoutAPI, check the other three consumers: the booking portal, the issues dashboard and the bookings sync admin page all call into it.
  • Verification is manual: with the flag off, run a full booking against the Opayo sandbox, then confirm the booking post reaches confirmed, the EP booking is confirmed, and the confirmation email/DotDigital push fires.
  • Deliberately not refactored: everything. This class is frozen pending removal of the legacy flow. Tidying it risks the rollback net for no product gain.

None. phpunit.xml.dist covers only inc/lovat-checkout/src, so nothing in this feature is under test. Treat every change as needing a manual end-to-end run.