Skip to content

Ownership property listings and search

Lovat Parks sells holiday homes as well as renting them. The ownership side of the site lists every caravan and lodge currently for sale, synced nightly from Elite Parks’ stock list, and lets a buyer filter by park, type, bedrooms and condition.

Stock is owned by EP. The website is a read-only mirror — nothing here writes a unit back.

Sales stock changes daily and is maintained by the parks in EP, not by marketers in WordPress. Rather than ask staff to double-key units, the site imports the stock list overnight and derives everything — listing pages, search, the [[property-xml-feed]] for portals — from that import.

The consequence, and the thing to understand before touching any of it: the nightly sync is destructive by design.

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

PathWhat it does
inc/eliteparks/ownership.php:110updateOwnershipProperties() — the nightly stock import.
inc/eliteparks/ownership.php:35UpdateEPOwnershipCoreData() — imports ownership lead sources from EP GetSetup.
inc/eliteparks/ownership.php:9POST /wp-json/ownership/v1/api/ — method=get_setup or method=update_properties. Manual triggers.
inc/eliteparks/ownership.php:14GET /wp-json/ownership/v1/search/ → getOwnershipProperties() — powers the search UI.
inc/eliteparks/ownership.php:247getOwnershipPropertiesData() — the actual filtering query.
single-property.phpThe listing page template (~31k, the largest single template in the theme).
blocks/ownership-search/, blocks/ownership-carousel/Search form and carousels.
partials/ownership-card.phpThe listing card.
inc/eliteparks/ownership.php:569Per-park pretty URLs, e.g. /ownership/search/<park-short-name>/.
flowchart TD
    A[daily_ownership_cron] --> B[EP OwnerAPI GetStockList]
    B --> C[Set EVERY published property to draft]
    C --> D{Unit exists by 'reference'?}
    D -->|no| E[wp_insert_post property, publish]
    D -->|yes| F[wp_update_post back to publish]
    E --> G[Write ACF fields, link grade_park by park code]
    F --> G
    G --> H[Search, carousels, single-property, XML feed]

daily_ownership_cron (registered ownership.php:560) runs updateOwnershipProperties(), which calls EP’s GetStockList on the OwnerAPI codeunit — a different namespace from the booking API, reached via new EPInterface('owner').

The algorithm:

  1. If EP returned any units at all, set every published property post to draft. This is how a sold unit disappears: it simply is not in tonight’s list, so it never gets un-drafted.
  2. For each unit in the response, match on the ACF field reference (EP’s no). Insert if new, otherwise update the title and set status back to publish.
  3. Write the ACF fields: bedrooms, manufacturer, manufacturer_code, model, model_code, year, width, length. recommended_selling_price is imported in the EP payload but the write is commented out (ownership.php:200).
  4. Link the unit to its park by looking up a park post whose code matches EP’s park_code, and store it in grade_park — the same field name the holiday grades use.

UpdateEPOwnershipCoreData() separately imports lead sources as leadsource posts with type = ownership, distinguishing them from the holiday-side lead sources.

getOwnershipPropertiesData($locationID, $propertyType, $propertyBedrooms, $propertyCondition, $limit = 1000) builds the filtered result set; getOwnershipProperties() is the REST wrapper the front end calls.

Per-park landing URLs are generated on init by custom_rewrite_rule(): it loops every published park, reads its short_name and code, and registers ^ownership/search/<short_name>/?$ → pagename=ownership/search&park=<code>. park is whitelisted as a query var and read back in pre_get_posts.

  • Buyer enquiries go through Gravity Forms into EP — see the forms sync (not yet documented).
  • Existing owners pay balances through [[booking-portal-balance-payments]].
  • Listings are syndicated by [[property-xml-feed]].
SettingWherePurpose
EP OwnerAPI credentialswp-config.phpSame OAuth pair as the booking API; the namespace differs, not the auth. See [[elite-parks-integration]].
park posts with code + short_nameContentcode links stock to a park; short_name generates the pretty search URL.
daily_ownership_cronWP-CronDaily. Registered at ownership.php:560.
ACF field group on propertyacf-json/The imported fields plus all marketing content.
  • The sync drafts every property before re-publishing. If EP returns a truncated or partial list, everything missing from it is silently unpublished. If the SOAP call succeeds but returns zero units, the guard at ownership.php:139 skips the mass-draft — but a partial response has no such protection. This is the single highest-impact failure mode in the feature.
  • reference (EP’s no) is the join key. Change it and the sync creates a duplicate post rather than updating the existing one.
  • The sync overwrites post_title every night. Editorial title changes do not survive.
  • Any ACF field the sync does not write is preserved — that is where marketing copy, images and pricing live. Adding a field to the sync silently makes it uneditable in the admin.
  • recommended_selling_price is deliberately not imported. The line is commented out at ownership.php:200; prices are managed in WordPress. Do not uncomment it without checking with the client.
  • Rewrite rules are generated from park content on every init. Adding a park, or changing its short_name, needs a permalink flush before the new URL works.
  • grade_park is shared with holiday grades. Code that reads grade_park may be handed a property or a grades post; functions.php:514 in the Yoast breadcrumb filter has to branch on post type for exactly this reason.
  • getOwnershipPropertiesData defaults to limit = 1000 — effectively unbounded. Fine at current stock levels; it is not paginated.
  • The sync is wp_insert_post/update_field in a loop with no batching. It is slow and it is a single cron tick.
  • New EP-sourced fields go in updateOwnershipProperties() next to the existing update_field() calls, and must also be added to single-property.php and, if buyers should see them off-site, the [[property-xml-feed]] exclusion list.
  • New filters go in getOwnershipPropertiesData(), keeping the existing positional signature or the REST wrapper breaks.
  • After changing rewrite rules, flush permalinks (Settings → Permalinks → Save, or wp rewrite flush).
  • Test the sync without waiting for cron: POST /wp-json/ownership/v1/api/?method=update_properties. Run it against staging first — on production it will draft-and-republish the entire catalogue.
  • Watch wp-content/uploads/logs/strategiq-ownership.log; it records the run plus units added/updated counts. A run reporting far fewer units than expected is the early warning for the truncation failure above.
  • Deliberately not abstracted: the draft-everything-then-republish pattern. It is crude, but it is the only way to detect a removal from an API that returns a full snapshot with no deletion events.

None. No PHPUnit coverage — the suite is scoped to inc/lovat-checkout/src only. The sync, the search query and the rewrite rules are all verified by hand.

Practical check after any change: run the sync on staging, compare SELECT COUNT(*) FROM wp_posts WHERE post_type='property' AND post_status='publish' before and after, and load a per-park search URL.