Lovat Parks architecture
Lovat Parks runs a group of UK holiday parks. The site does two commercial jobs: it sells holidays (search → book → pay) and it sells holiday homes (browse stock → enquire → own). Both sit on top of a single external system — Elite Parks — which is the source of truth for everything that matters.
This page is the map. The feature pages are the territory.
Shape of the system
Section titled “Shape of the system”A single WordPress install (WP Engine, deployed from Bitbucket) with one custom theme, lovat-parks, derived from the StrategiQ base theme. Almost all custom behaviour lives in the theme, not in plugins.
flowchart TD
subgraph EP[Elite Parks - Microsoft Dynamics]
EP1[BookingAPI codeunit]
EP2[OwnerAPI codeunit]
end
subgraph WP[WordPress - theme lovat-parks]
S[Holiday search]
LC[Legacy checkout - booking CPT]
WC[WooCommerce checkout]
DEP[Deposit choice]
REC[Reconciliation queue]
OWN[Ownership listings]
POR[Booking portal]
FEED[Property XML feed]
end
subgraph PAY[Payments]
OP[Opayo - legacy]
CS[Cardstream]
WCG[WooCommerce gateways]
end
S --> EP1
S -->|SCID| LC
S -->|SCID| WC
LC --> EP1
LC --> OP
WC --> EP1
WC --> WCG
WC --> DEP
WC --> REC
REC --> EP1
POR --> EP1
POR --> CS
OWN --> EP2
OWN --> FEED
The one idea to hold on to
Section titled “The one idea to hold on to”Elite Parks owns the truth; WordPress owns the presentation. No booking, price or unit of stock originates here. That is why:
- Search results are a cache in custom MySQL tables, not content.
- A reservation is a 20-minute EP “tag”, not a database lock.
- Money can be taken and the booking still fail, which is why a reconciliation queue exists.
- The nightly ownership sync unpublishes everything and republishes what EP still lists.
Feature map
Section titled “Feature map”Booking — the money path
Section titled “Booking — the money path”| Feature | What it covers |
|---|---|
| [[elite-parks-integration]] | The SOAP client every other feature depends on. Read this first. |
| [[holiday-search-and-availability]] | Availability search, the SCID, the booking_search_results cache. |
| [[woocommerce-holiday-checkout]] | The current flow: multi-holiday cart, ghost products, per-line holds, extras, promos. |
| [[legacy-holiday-checkout]] | The original Opayo flow. Frozen, still live as the rollback path. |
| [[deposit-and-pay-in-full]] | Deposit-or-full choice and the wc-deposit-paid status. |
| [[booking-reconciliation-queue]] | What happens when payment succeeds and EP does not. |
Ownership — selling holiday homes
Section titled “Ownership — selling holiday homes”| Feature | What it covers |
|---|---|
| [[ownership-property-listings]] | Nightly stock sync, property pages, per-park search URLs. |
| [[booking-portal-balance-payments]] | Cardstream portal for paying an outstanding balance. |
| [[property-xml-feed]] | Public XML syndication of sales stock. |
Not yet documented
Section titled “Not yet documented”Real features, deliberately out of scope for the first audit pass:
- Gravity Forms → Elite Parks lead sync (
inc/eliteparks/forms.php, 5-minute cron,leadsourceCPT) - DotDigital sync (
inc/eliteparks/bookings-sync.php,inc/woocommerce/dotdigital-sync.php) - Reviews import (Feefo / BrightLocal,
inc/reviews/, hourly + daily crons) - Content model — 9 CPTs and 12 taxonomies in
inc/cpt.php; parks, locations, grades, offers - ACF block system — ~50 blocks under
blocks/, registered byinc/register-blocks.php - Admin tooling — issues dashboard, booking activity log, bookings sync page, tagging dashboard
- Site search, export API, build & deploy
Cross-cutting mechanics
Section titled “Cross-cutting mechanics”The SCID
Section titled “The SCID”A GUID identifying one bookable option at one price, generated when a search runs. It is the handle passed from search into either checkout, and the key booking_search_results is indexed on. A SCID is not a reservation — nothing is held in EP until a checkout tags it, and a stale SCID still resolves to a stale price.
Feature flags
Section titled “Feature flags”The WooCommerce migration is flag-controlled, and the distinction between the two accessors matters more than anything else in the codebase:
| Accessor | Applies admin-only gate? | Use for |
|---|---|---|
Config::isWooCommerceEnabled() | Yes | Front-end rendering and routing |
Config::isWooCommerceFeatureActive() | No | Payment hooks, cron, gateway callbacks — anywhere with no logged-in user |
Resolution order for both: the LP_USE_WOOCOMMERCE_CHECKOUT constant wins if defined; otherwise the ACF option wc_checkout_enabled, optionally narrowed by wc_checkout_admin_only. functions.php deliberately does not define the constant so the ACF toggle can work.
Custom tables
Section titled “Custom tables”Not everything is a post. {prefix}booking_searches, {prefix}booking_search_results, {prefix}booking_grades and the unprefixed bookingseasons are plain MySQL, created by dbDelta and evolved by ad-hoc ALTER TABLE guards on init. There are no migrations.
Scheduled work
Section titled “Scheduled work”| Hook | Interval | Feature |
|---|---|---|
untagBookingsFromEP | 1 min | [[legacy-holiday-checkout]] |
check_for_new_form_entries_event | 5 min | Forms → EP sync |
lovat_booking_reconcile | 30 min | [[booking-reconciliation-queue]] |
reviews_cron_hook | hourly | Reviews import |
cleanup_results_cron | hourly | [[holiday-search-and-availability]] |
daily_booking_cron | daily | EP core data refresh |
daily_ownership_cron | daily | [[ownership-property-listings]] |
average_score_cron_hook | daily | Reviews |
lovat_ghost_product_cleanup | daily | [[woocommerce-holiday-checkout]] |
WP-Crontrol is installed for inspecting these.
Logging
Section titled “Logging”Two conventions coexist:
- New code — the WooCommerce logger,
wp-content/uploads/wc-logs/, sourceslovat-checkout,lovat-eliteparks(90-day retention, PII-sanitised),lovat-portal-cardstream. - Legacy code — hand-rolled files in
wp-content/uploads/logs/:strategiq-booking.log,strategiq-checkout.log,strategiq-tagging.log,strategiq-ownership.log,strategiq-cleanup.log,ep-payloads.log.
When debugging a booking, check both.
Code layout
Section titled “Code layout”wp-content/themes/lovat-parks/├── blocks/ ~50 ACF Gutenberg blocks (php + js + scss per block)├── inc/│ ├── cpt.php 9 CPTs, 12 taxonomies│ ├── eliteparks/ Legacy EP integration — booking, ownership, forms, admin tools│ ├── lovat-checkout/ New WooCommerce flow (PSR-4, namespaced, tested)│ │ └── src/ Cart, Checkout, EliteParks, Extras, Order, Portal, Product, Reconciliation, Routing│ ├── opayo/ Legacy gateway│ ├── reviews/, cleanup.php, export.php, property-xml-feed.php, search.php …├── partials/, parts/, templates/├── tests/unit/ PHPUnit — covers inc/lovat-checkout/src only├── cli/ WP-CLI block scaffolding + refactor tools└── resources/ + assets/ SCSS/JS source → compiled by gulpThe split is generational, not architectural: inc/eliteparks/ is procedural legacy, inc/lovat-checkout/ is the modern PSR-4 code with tests. New work belongs in the second.
Health of the codebase
Section titled “Health of the codebase”Honest notes for whoever inherits this:
composer testis currently red — 91 tests, 15 errors, 2 failures onmaster. The failures are test drift, not broken product code (for exampleDepositCalculatorTest::testMultiLineMixedEligibilitypredates the “no line may lack a deposit” and £5-minimum rules;ExtrasPageControllerTestlacks a Brain Monkey stub fornocache_headers()). Fix the tests before relying on the suite as a gate.- Test coverage is narrow. Reconciliation, the portal, the cart REST endpoints, all of search and everything legacy have no tests at all.
inc/eliteparks/booking.phpis ~4,100 lines andcheckoutAPI.php~2,570. Both far exceed any sane file limit. They are frozen rather than refactored, on purpose.- Two checkouts, two gateways and two EP clients run in parallel. That is the cost of a safe migration; it is not permanent. When the WooCommerce flow is confirmed stable the legacy path should be removed, and these pages updated.
Getting started
Section titled “Getting started”See [[onboarding]].