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.
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”All paths relative to wp-content/themes/lovat-parks/.
| Path | What it does |
|---|---|
inc/eliteparks/ownership.php:110 | updateOwnershipProperties() — the nightly stock import. |
inc/eliteparks/ownership.php:35 | UpdateEPOwnershipCoreData() — imports ownership lead sources from EP GetSetup. |
inc/eliteparks/ownership.php:9 | POST /wp-json/ownership/v1/api/ — method=get_setup or method=update_properties. Manual triggers. |
inc/eliteparks/ownership.php:14 | GET /wp-json/ownership/v1/search/ → getOwnershipProperties() — powers the search UI. |
inc/eliteparks/ownership.php:247 | getOwnershipPropertiesData() — the actual filtering query. |
single-property.php | The listing page template (~31k, the largest single template in the theme). |
blocks/ownership-search/, blocks/ownership-carousel/ | Search form and carousels. |
partials/ownership-card.php | The listing card. |
inc/eliteparks/ownership.php:569 | Per-park pretty URLs, e.g. /ownership/search/<park-short-name>/. |
How it works
Section titled “How it works”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]
The nightly sync
Section titled “The nightly sync”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:
- If EP returned any units at all, set every published
propertypost todraft. This is how a sold unit disappears: it simply is not in tonight’s list, so it never gets un-drafted. - For each unit in the response, match on the ACF field
reference(EP’sno). Insert if new, otherwise update the title and set status back topublish. - Write the ACF fields:
bedrooms,manufacturer,manufacturer_code,model,model_code,year,width,length.recommended_selling_priceis imported in the EP payload but the write is commented out (ownership.php:200). - Link the unit to its park by looking up a
parkpost whosecodematches EP’spark_code, and store it ingrade_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.
Search
Section titled “Search”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.
Related
Section titled “Related”- 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]].
Configuration
Section titled “Configuration”| Setting | Where | Purpose |
|---|---|---|
| EP OwnerAPI credentials | wp-config.php | Same OAuth pair as the booking API; the namespace differs, not the auth. See [[elite-parks-integration]]. |
park posts with code + short_name | Content | code links stock to a park; short_name generates the pretty search URL. |
daily_ownership_cron | WP-Cron | Daily. Registered at ownership.php:560. |
ACF field group on property | acf-json/ | The imported fields plus all marketing content. |
Invariants and gotchas
Section titled “Invariants and gotchas”- 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:139skips the mass-draft — but a partial response has no such protection. This is the single highest-impact failure mode in the feature. reference(EP’sno) is the join key. Change it and the sync creates a duplicate post rather than updating the existing one.- The sync overwrites
post_titleevery 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_priceis deliberately not imported. The line is commented out atownership.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 itsshort_name, needs a permalink flush before the new URL works. grade_parkis shared with holiday grades. Code that readsgrade_parkmay be handed apropertyor agradespost;functions.php:514in the Yoast breadcrumb filter has to branch on post type for exactly this reason.getOwnershipPropertiesDatadefaults tolimit = 1000— effectively unbounded. Fine at current stock levels; it is not paginated.- The sync is
wp_insert_post/update_fieldin a loop with no batching. It is slow and it is a single cron tick.
Changing it safely
Section titled “Changing it safely”- New EP-sourced fields go in
updateOwnershipProperties()next to the existingupdate_field()calls, and must also be added tosingle-property.phpand, 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.