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.
Why it exists
Section titled “Why it exists”It was the site’s only checkout until the 2026 migration. It is kept in place — not deleted — for two reasons:
- Rollback. Turning off
wc_checkout_enabledmust restore a working checkout instantly. That only works if this code path stays untouched. - History. Every booking taken before the migration is a
bookingpost. 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.
Entry points
Section titled “Entry points”All paths relative to wp-content/themes/lovat-parks/.
| Path | What it does |
|---|---|
blocks/holiday-checkout/block-holiday-checkout.php | The 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.php | Thank-you page. Confirms the payment, renders the receipt, pushes to DotDigital. |
inc/eliteparks/checkoutAPI.php:7 | CheckoutAPI — the ~2,500-line class that does everything. |
inc/eliteparks/checkoutBooking.php:5 | CheckoutBooking — builds the initial page payload for Vue from cached search data. |
inc/eliteparks/booking.php:1625 | getData() — 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.php | Opayo/Elavon integration. OpayoApi-local.php is the local-dev variant. |
How it works
Section titled “How it works”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]
Local booking record
Section titled “Local booking record”CheckoutAPI::tag_local_scid() (checkoutAPI.php:270) is the first thing the block calls. It:
- Looks up the SCID in the search results (see [[holiday-search-and-availability]]).
- Creates a
bookingpost if one does not already exist for that SCID (create_local_booking, post title = the SCID). - Writes
scidback onto the booking and the booking id onto the search result — into thebooking_search_results.booking_idcolumn whenenable_v2_searchis on, or ACF meta on the result post when it is not. - Seeds
booking_status = created.
booking_status drives the whole lifecycle: created → pending_payment → confirmed, with booking_tag_date / booking_untag_date recording the hold.
Payment
Section titled “Payment”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.
Hold expiry
Section titled “Hold expiry”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).
Confirmation and DotDigital
Section titled “Confirmation and DotDigital”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.
Configuration
Section titled “Configuration”| Setting | Where | Purpose |
|---|---|---|
SAGEPAY_IS_PRODUCTION | wp-config.php | Live vs sandbox Opayo JS. |
OPAYO_CONFIG | wp-config.php | Per-park Opayo credentials array, keyed by park code. Names only — never record values. |
checkout_page, confirmation_page, search_page, portal_page | ACF options | Where the flow redirects. |
enable_v2_search | ACF option | Switches the booking↔search-result link between the flat table and ACF meta. |
wc_checkout_enabled / LP_USE_WOOCOMMERCE_CHECKOUT | ACF option / constant | When true this whole flow is bypassed. See [[woocommerce-holiday-checkout]]. |
| EP credentials | wp-config.php | See [[elite-parks-integration]]. |
Invariants and gotchas
Section titled “Invariants and gotchas”- 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.
CheckoutAPIis 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_dateandprevious_part_payments_madeare ACF fields, and out-of-order writes corrupt the lifecycle.previous_part_payments_madeis a comma-joined string, and duplicate-payment protection is anin_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.
Changing it safely
Section titled “Changing it safely”- 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
methodbranches ingetData()(booking.php:1625) — match the existingjson_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
bookingpost reachesconfirmed, 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.