<?php
/*
Plugin Name: Buildium Units Sync (Allowlist + Relationship Repair + Rollups + Daily)
Description: Sync Buildium MarketRent + IsUnitListed into Pods units (CPT: units). Only sync units that exist in WP (allowlist). Repairs unit->property + unit->floorplan relationships. Repairs floorplan->property. Recalculates has_available_units + price range rollups. Daily WP-Cron. Manual: /wp-admin/?sync_buildium=1
Version: 4.2
*/
if (!defined('ABSPATH')) exit;
define('BUILDIUM_SYNC_CRON_HOOK', 'buildium_units_sync_daily_hook');
/**
* Pods pick fields on Units (separate, correct relationships)
* Units.properties => property post ID(s)
* Units.floorplans => floorplan post ID(s)
*/
define('CH_UNIT_PROP_FIELD', 'properties');
define('CH_UNIT_FLOORPLAN_FIELD', 'floorplans');
/**
* Legacy mixed relationship field on Units/Floorplans (optional, for backward compatibility)
* Units.relationship can include BOTH property IDs and floorplan IDs.
* Floorplans.relationship includes Property ID.
*/
define('CH_REL_FIELD', 'relationship');
define('CH_UPDATE_LEGACY_REL_FIELD', true); // set false if you want to stop syncing "relationship"
/**
* ---- CONFIG ----
* Adjust these CPT slugs only if yours differ.
*/
define('CH_UNITS_CPT', 'units');
define('CH_FLOORPLANS_CPT', 'floorplans');
define('CH_PROPERTIES_CPT', 'properties');
/**
* Availability field slugs
*/
define('CH_AVAIL_FIELD', 'has_available_units'); // on properties + floorplans
/**
* Price field slugs (rollup string like "$850–$2,000 per month")
*/
define('CH_PRICE_FIELD', 'price'); // on properties + floorplans
/**
* Buildium meta keys
*/
define('CH_PROPERTY_BUILD_META', 'buildium_property_id'); // on properties
define('CH_UNIT_BUILD_META', 'buildium_unit_id'); // on units
/**
* Relationship repair behavior
* If true: every sync_buildium run and daily cron will also repair relationships.
* If false: relationships only repair when you hit /wp-admin/?repair_buildium_relationships=1
*/
define('CH_REPAIR_RELATIONSHIPS_ON_SYNC', true);
/**
* Floorplan->Property repair behavior (uses floorplan title prefix before "|")
*/
define('CH_REPAIR_FLOORPLAN_PROPERTY_ON_SYNC', true);
/** =========================================================
* Helpers
* ========================================================= */
if (!function_exists('ch_str_contains')) {
function ch_str_contains($haystack, $needle) {
if ($needle === '') return true;
return (strpos((string)$haystack, (string)$needle) !== false);
}
}
/**
* Normalize Pods relationship field output into a flat array of IDs.
*/
function ch_normalize_pods_rel_ids($rel) {
$ids = array();
if (empty($rel)) return $ids;
if (is_numeric($rel)) {
$ids[] = (int)$rel;
return $ids;
}
if (is_array($rel)) {
foreach ($rel as $item) {
if (is_numeric($item)) {
$ids[] = (int)$item;
} elseif (is_array($item) && isset($item['ID'])) {
$ids[] = (int)$item['ID'];
} elseif (is_object($item) && isset($item->ID)) {
$ids[] = (int)$item->ID;
}
}
}
$ids = array_filter(array_unique(array_map('intval', $ids)));
return array_values($ids);
}
/** =========================================================
* ACTIVATE / DEACTIVATE CRON
* ========================================================= */
register_activation_hook(__FILE__, function () {
if (!wp_next_scheduled(BUILDIUM_SYNC_CRON_HOOK)) {
wp_schedule_event(time() + 300, 'daily', BUILDIUM_SYNC_CRON_HOOK);
}
});
register_deactivation_hook(__FILE__, function () {
$ts = wp_next_scheduled(BUILDIUM_SYNC_CRON_HOOK);
if ($ts) wp_unschedule_event($ts, BUILDIUM_SYNC_CRON_HOOK);
});
/** =========================================================
* MANUAL TRIGGERS
* ========================================================= */
add_action('admin_init', function () {
// Duplicate report
if (isset($_GET['sync_buildium_duplicates'])) {
if (!current_user_can('manage_options')) wp_die('Not allowed.');
$trash = isset($_GET['trash']) && $_GET['trash'] == '1';
$debug = isset($_GET['debug']) && $_GET['debug'] == '1';
echo '<h2>=== BUILDIUM DUPLICATE REPORT ===</h2>';
$report = buildium_find_duplicate_units($trash, $debug);
echo '<pre>' . esc_html(print_r($report, true)) . '</pre>';
echo '<h2>=== DONE ===</h2>';
exit;
}
// Relationship repair only (manual)
if (isset($_GET['repair_buildium_relationships'])) {
if (!current_user_can('manage_options')) wp_die('Not allowed.');
$debug = isset($_GET['debug']) && $_GET['debug'] == '1';
$dry_run = isset($_GET['dry_run']) && $_GET['dry_run'] == '1';
echo '<h2>=== BUILDIUM RELATIONSHIP REPAIR START ===</h2>';
echo '<p>Dry run: <b>' . ($dry_run ? 'YES' : 'NO') . '</b> | Debug: <b>' . ($debug ? 'ON' : 'OFF') . '</b></p>';
$summary = ch_repair_unit_relationships_from_buildium($debug, $dry_run);
echo '<h2>=== BUILDIUM RELATIONSHIP REPAIR COMPLETE ===</h2>';
echo '<pre>' . esc_html(print_r($summary, true)) . '</pre>';
exit;
}
// Floorplan->Property repair only (manual)
if (isset($_GET['repair_floorplan_properties'])) {
if (!current_user_can('manage_options')) wp_die('Not allowed.');
$debug = isset($_GET['debug']) && $_GET['debug'] == '1';
$dry_run = isset($_GET['dry_run']) && $_GET['dry_run'] == '1';
echo '<h2>=== FLOORPLAN → PROPERTY REPAIR START ===</h2>';
echo '<p>Dry run: <b>' . ($dry_run ? 'YES' : 'NO') . '</b> | Debug: <b>' . ($debug ? 'ON' : 'OFF') . '</b></p>';
$summary = ch_repair_floorplan_property_relationships($debug, $dry_run);
echo '<h2>=== FLOORPLAN → PROPERTY REPAIR COMPLETE ===</h2>';
echo '<pre>' . esc_html(print_r($summary, true)) . '</pre>';
exit;
}
// Manual sync (supports dry_run)
if (!isset($_GET['sync_buildium'])) return;
if (!current_user_can('manage_options')) wp_die('Not allowed.');
$debug = isset($_GET['debug']) && $_GET['debug'] == '1';
$dry_run = isset($_GET['dry_run']) && $_GET['dry_run'] == '1';
echo '<h2>=== BUILDIUM SYNC START ===</h2>';
echo '<p>Dry run: <b>' . ($dry_run ? 'YES (no writes)' : 'NO (will write)') . '</b> | Debug: <b>' . ($debug ? 'ON' : 'OFF') . '</b></p>';
$summary = buildium_units_sync_run_all($debug, $dry_run);
echo '<h2>=== BUILDIUM SYNC COMPLETE ===</h2>';
echo '<pre>' . esc_html(print_r($summary, true)) . '</pre>';
exit;
});
/** =========================================================
* DAILY CRON SYNC
* ========================================================= */
add_action(BUILDIUM_SYNC_CRON_HOOK, function () {
buildium_units_sync_run_all(false, false);
});
/** =========================================================
* CORE: SYNC ALL (PAGINATED) + OPTIONAL REL REPAIR + ROLLUPS
* ========================================================= */
function buildium_units_sync_run_all($debug = false, $dry_run = false) {
global $wpdb;
$pods_active = function_exists('pods');
// Require credentials from wp-config.php
if (!defined('BUILDIUM_CLIENT_ID') || !defined('BUILDIUM_CLIENT_SECRET')) {
if ($debug) {
echo '<p><b>Missing Buildium credentials.</b> Define BUILDIUM_CLIENT_ID and BUILDIUM_CLIENT_SECRET in wp-config.php</p>';
}
return array(
'ok' => false,
'error' => 'Missing Buildium credentials (BUILDIUM_CLIENT_ID / BUILDIUM_CLIENT_SECRET).',
'pods_active' => $pods_active
);
}
$client_id = BUILDIUM_CLIENT_ID;
$client_secret = BUILDIUM_CLIENT_SECRET;
$base_url = 'https://api.buildium.com/v1/rentals/units';
$limit = 50;
$offset = 0;
$max_pages = 600;
$checked_buildium = 0;
$matched_wp = 0;
$updated_wp = 0;
$skipped_not_imported = 0;
$errors = 0;
// Relationship repair stats
$rel_summary = array(
'enabled' => CH_REPAIR_RELATIONSHIPS_ON_SYNC ? 1 : 0,
'unit_rel_checked' => 0,
'unit_rel_updated' => 0,
'no_property_match' => 0,
'no_floorplan_match' => 0,
);
// Floorplan->Property repair stats
$fp_prop_summary = array(
'enabled' => (CH_REPAIR_FLOORPLAN_PROPERTY_ON_SYNC ? 1 : 0),
'checked' => 0,
'updated' => 0,
'no_match' => 0,
);
// Cache: buildium_id => post_id
$match_cache = array();
/**
* ALLOWLIST:
* Only sync Buildium unit IDs that exist in WP already (your imported units).
*/
$allowed_buildium_ids = array();
$existing_ids = $wpdb->get_col("
SELECT pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '" . esc_sql(CH_UNIT_BUILD_META) . "'
AND p.post_type = '" . esc_sql(CH_UNITS_CPT) . "'
AND pm.meta_value <> ''
");
if (!empty($existing_ids)) {
foreach ($existing_ids as $bid) {
$allowed_buildium_ids[(string)$bid] = true;
}
}
// Build lookups used for relationship repair (only if enabled)
$property_lookup = array();
$floorplan_lookup = array();
if (CH_REPAIR_RELATIONSHIPS_ON_SYNC && $pods_active) {
$property_lookup = ch_build_property_lookup();
$floorplan_lookup = ch_build_floorplan_title_lookup(); // now case-insensitive
}
/**
* Rollup accumulators:
* - Availability: based on IsUnitListed==1 (your current meaning)
* - Price range: based on IsUnitListed==1 (listed units, even if otherwise "unavailable")
*/
$floorplan_has_any = array(); // floorplan_id => true
$property_has_any = array(); // property_id => true
$fp_min = array(); // floorplan_id => float
$fp_max = array(); // floorplan_id => float
$pr_min = array(); // property_id => float
$pr_max = array(); // property_id => float
for ($page = 1; $page <= $max_pages; $page++) {
$url = add_query_arg(array('limit' => $limit, 'offset' => $offset), $base_url);
$response = wp_remote_get($url, array(
'headers' => array(
'x-buildium-client-id' => $client_id,
'x-buildium-client-secret' => $client_secret,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'WordPress Buildium Sync'
),
'timeout' => 30
));
if (is_wp_error($response)) {
$errors++;
if ($debug) echo '<p><b>API ERROR:</b> ' . esc_html($response->get_error_message()) . '</p>';
break;
}
$status = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if ($status !== 200) {
$errors++;
if ($debug) {
echo '<p><b>HTTP ' . intval($status) . '</b></p>';
echo '<pre>' . esc_html(substr($body, 0, 1200)) . '</pre>';
}
break;
}
$data = json_decode($body, true);
if ($data === null) {
$errors++;
if ($debug) echo '<p><b>JSON decode failed</b></p><pre>' . esc_html(substr($body, 0, 1200)) . '</pre>';
break;
}
// End condition
if (is_array($data) && empty($data)) {
if ($debug) echo '<pre>Reached end of results (empty array). Stop.</pre>';
break;
}
// Support BOTH response shapes
if (isset($data['Results']) && is_array($data['Results'])) {
$units = $data['Results'];
} elseif (is_array($data) && isset($data[0]) && is_array($data[0])) {
$units = $data;
} else {
$errors++;
if ($debug) echo '<p><b>Unexpected response structure</b></p><pre>' . esc_html(print_r($data, true)) . '</pre>';
break;
}
if ($debug) echo '<pre>Page ' . intval($page) . ' offset=' . intval($offset) . ' units=' . intval(count($units)) . '</pre>';
if (empty($units)) break;
foreach ($units as $unit) {
if (!isset($unit['Id'])) continue;
$buildium_id = (string)$unit['Id'];
// Skip Buildium units that you didn't import into WP (commercial / irrelevant)
if (!isset($allowed_buildium_ids[$buildium_id])) {
$skipped_not_imported++;
continue;
}
$checked_buildium++;
// Buildium fields
$new_rent = $unit['MarketRent'] ?? null;
$new_listed = $unit['IsUnitListed'] ?? null;
// Normalize
$rent_norm = is_null($new_rent) ? '' : (string)$new_rent;
if (is_bool($new_listed)) $listed_norm = $new_listed ? '1' : '0';
elseif (is_numeric($new_listed)) $listed_norm = (string)intval($new_listed);
elseif (is_string($new_listed) && ($new_listed === 'true' || $new_listed === 'false')) $listed_norm = ($new_listed === 'true') ? '1' : '0';
else $listed_norm = (string)$new_listed;
if ($debug) {
echo '<hr><b>Processing Buildium Unit ID:</b> ' . esc_html($buildium_id) . '<br>';
}
// Find matching WP post_id (cache)
if (isset($match_cache[$buildium_id])) {
$post_id = $match_cache[$buildium_id];
} else {
$query = new WP_Query(array(
'post_type' => CH_UNITS_CPT,
'posts_per_page' => 1,
'fields' => 'ids',
'post_status' => 'any',
'meta_query' => array(
array(
'key' => CH_UNIT_BUILD_META,
'value' => $buildium_id,
'compare' => '='
)
),
'no_found_rows' => true,
));
if (empty($query->posts)) {
if ($debug) echo '❌ No WP unit found for buildium_unit_id = ' . esc_html($buildium_id) . '<br>';
continue;
}
$post_id = intval($query->posts[0]);
$match_cache[$buildium_id] = $post_id;
}
$matched_wp++;
/**
* ---- RELATIONSHIP REPAIR (optional) ----
* Writes to:
* - Units.properties
* - Units.floorplans
* - (optional) Units.relationship legacy field
*/
if (CH_REPAIR_RELATIONSHIPS_ON_SYNC && $pods_active) {
$rel_summary['unit_rel_checked']++;
$prop_buildium_id = isset($unit['PropertyId']) ? (string)$unit['PropertyId'] : '';
$bedrooms_raw = $unit['UnitBedrooms'] ?? null;
$bathrooms_raw = $unit['UnitBathrooms'] ?? null;
$rr = ch_repair_one_unit_relationships(
$post_id,
$prop_buildium_id,
$bedrooms_raw,
$bathrooms_raw,
$property_lookup,
$floorplan_lookup,
$debug,
($dry_run ? false : true) // respect dry_run
);
if (!empty($rr['updated'])) $rel_summary['unit_rel_updated']++;
if (!empty($rr['no_property_match'])) $rel_summary['no_property_match']++;
if (!empty($rr['no_floorplan_match'])) $rel_summary['no_floorplan_match']++;
}
// Always compute relationships for rollups (even if values didn't change)
$rels = ch_get_unit_related_ids($post_id, $pods_active);
$unit_is_listed = ((string)$listed_norm === '1');
// Availability rollups: treat "listed" as available flag (as per your existing behavior)
if ($unit_is_listed) {
foreach ($rels['floorplans'] as $fid) $floorplan_has_any[(int)$fid] = true;
foreach ($rels['properties'] as $pid) $property_has_any[(int)$pid] = true;
}
// Price range rollups (listed units)
$rent_val = is_numeric($rent_norm) ? floatval($rent_norm) : null;
if ($unit_is_listed && $rent_val !== null) {
foreach ($rels['floorplans'] as $fid) {
$fid = (int)$fid;
if (!isset($fp_min[$fid]) || $rent_val < $fp_min[$fid]) $fp_min[$fid] = $rent_val;
if (!isset($fp_max[$fid]) || $rent_val > $fp_max[$fid]) $fp_max[$fid] = $rent_val;
}
foreach ($rels['properties'] as $pid) {
$pid = (int)$pid;
if (!isset($pr_min[$pid]) || $rent_val < $pr_min[$pid]) $pr_min[$pid] = $rent_val;
if (!isset($pr_max[$pid]) || $rent_val > $pr_max[$pid]) $pr_max[$pid] = $rent_val;
}
}
// Current values
$old_rent = (string)get_post_meta($post_id, 'marketrent', true);
$old_listed = (string)get_post_meta($post_id, 'isunitlisted', true);
// Decide if changes
$changed = false;
if ((string)$rent_norm !== $old_rent) $changed = true;
if ((string)$listed_norm !== $old_listed) $changed = true;
if ($debug) {
echo '✅ Matched WP Post ID: ' . intval($post_id) . '<br>';
echo 'Current marketrent: '; var_dump($old_rent);
echo 'Current isunitlisted: '; var_dump($old_listed);
echo 'API MarketRent: '; var_dump($new_rent);
echo 'API IsUnitListed: '; var_dump($new_listed);
echo 'Normalized rent/listed: '; var_dump($rent_norm); var_dump($listed_norm);
echo 'Changed?: ' . ($changed ? 'YES' : 'NO') . '<br>';
}
if (!$changed) {
if ($debug) echo '— No change<br>';
continue;
}
if ($dry_run) {
if ($debug) echo 'DRY RUN: would update marketrent/isunitlisted<br>';
continue;
}
// 1) Update meta
update_post_meta($post_id, 'marketrent', $rent_norm);
update_post_meta($post_id, 'isunitlisted', $listed_norm);
// 2) Pods save fallback (recommended)
if ($pods_active) {
$pod = pods(CH_UNITS_CPT, $post_id);
if ($pod && method_exists($pod, 'save')) {
$pod->save(array(
'marketrent' => $rent_norm,
'isunitlisted' => $listed_norm,
));
}
}
clean_post_cache($post_id);
$updated_wp++;
if ($debug) {
$verify_rent = get_post_meta($post_id, 'marketrent', true);
$verify_listed = get_post_meta($post_id, 'isunitlisted', true);
echo 'After update marketrent: '; var_dump($verify_rent);
echo 'After update isunitlisted: '; var_dump($verify_listed);
}
}
$offset += $limit;
}
/**
* ---- ROLLUPS: set has_available_units on floorplans + properties ----
*/
$recalc_floorplans = ch_update_has_available_units(CH_FLOORPLANS_CPT, $floorplan_has_any, $pods_active, $debug, $dry_run);
$recalc_properties = ch_update_has_available_units(CH_PROPERTIES_CPT, $property_has_any, $pods_active, $debug, $dry_run);
/**
* ---- PRICE RANGE ROLLUPS: set price on floorplans + properties ----
*/
$recalc_floorplan_price = ch_update_price_range_rollup(CH_FLOORPLANS_CPT, $fp_min, $fp_max, $pods_active, $debug, $dry_run);
$recalc_property_price = ch_update_price_range_rollup(CH_PROPERTIES_CPT, $pr_min, $pr_max, $pods_active, $debug, $dry_run);
/**
* ---- FLOORPLAN -> PROPERTY REPAIR (optional) ----
*/
$floorplan_property_repairs = array('ok'=>true,'skipped'=>1);
if (CH_REPAIR_FLOORPLAN_PROPERTY_ON_SYNC && $pods_active) {
$floorplan_property_repairs = ch_repair_floorplan_property_relationships($debug, $dry_run);
}
return array(
'ok' => ($errors === 0),
'dry_run' => $dry_run ? 1 : 0,
'pods_active' => $pods_active,
'checked_buildium_units_allowlisted' => $checked_buildium,
'skipped_not_imported' => $skipped_not_imported,
'matched_wp_units' => $matched_wp,
'updated_wp_units' => $updated_wp,
'errors' => $errors,
'relationship_repair' => $rel_summary,
'recalc_floorplans' => $recalc_floorplans,
'recalc_properties' => $recalc_properties,
'recalc_floorplan_price' => $recalc_floorplan_price,
'recalc_property_price' => $recalc_property_price,
'floorplan_property_repairs' => $floorplan_property_repairs,
);
}
/** =========================================================
* LOOKUPS
* ========================================================= */
/**
* Build lookup map: buildium_property_id => property post ID
*/
function ch_build_property_lookup() {
global $wpdb;
$rows = $wpdb->get_results("
SELECT pm.meta_value AS bid, pm.post_id AS pid
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_type = '" . esc_sql(CH_PROPERTIES_CPT) . "'
AND pm.meta_key = '" . esc_sql(CH_PROPERTY_BUILD_META) . "'
AND pm.meta_value <> ''
", ARRAY_A);
$map = [];
foreach ($rows as $r) {
$bid = (string)$r['bid'];
$map[$bid] = (int)$r['pid'];
}
return $map;
}
/**
* Build lookup map: floorplan title (lowercase) => floorplan post ID
*/
function ch_build_floorplan_title_lookup() {
$q = new WP_Query([
'post_type' => CH_FLOORPLANS_CPT,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
'no_found_rows' => true,
]);
$map = [];
foreach ($q->posts as $fid) {
$title = get_the_title($fid);
if ($title) $map[strtolower(trim($title))] = (int)$fid;
}
return $map;
}
/**
* Use FULL property title as floorplan prefix:
* "Aurelia Flats" => "Aurelia Flats"
*/
function ch_property_prefix_from_title($property_post_id) {
$title = trim(get_the_title($property_post_id));
if ($title === '') return '';
// ✅ Minimal hard mapping for known exceptions
$map = [
'Windsor Tower' => 'Windsor',
'West Market Townhomes' => 'West Market',
];
if (isset($map[$title])) return $map[$title];
// Default: first token
$parts = preg_split('/[\s\-|]+/', $title);
return isset($parts[0]) ? trim($parts[0]) : $title;
}
/**
* Normalize Buildium bedroom/bathroom fields to your naming.
* Supports enums like OneBed/TwoBed/Studio and OneBath/TwoBath/OneHalfBath, etc.
*/
function ch_floorplan_label_from_unit($bedrooms_raw, $bathrooms_raw) {
// Bedrooms -> "Studio" or "1 Bed" etc.
$bed_label = '';
if (is_string($bedrooms_raw)) {
$br = strtolower(trim($bedrooms_raw));
if ($br === 'studio') $bed_label = 'Studio';
elseif ($br === 'onebed') $bed_label = '1 Bed';
elseif ($br === 'twobed') $bed_label = '2 Bed';
elseif ($br === 'threebed') $bed_label = '3 Bed';
elseif ($br === 'fourbed') $bed_label = '4 Bed';
// fallback contains logic
elseif (ch_str_contains($br, 'studio')) $bed_label = 'Studio';
elseif (ch_str_contains($br, 'one')) $bed_label = '1 Bed';
elseif (ch_str_contains($br, 'two')) $bed_label = '2 Bed';
elseif (ch_str_contains($br, 'three')) $bed_label = '3 Bed';
elseif (ch_str_contains($br, 'four')) $bed_label = '4 Bed';
}
if ($bed_label === '' && is_numeric($bedrooms_raw)) {
$bed_label = intval($bedrooms_raw) . ' Bed';
}
// Bathrooms -> "1 Bath" / "2 Bath" (mainly for special titles like "2 Bed | 2 Bath")
$bath_label = '';
if (is_string($bathrooms_raw)) {
$ba = strtolower(trim($bathrooms_raw));
if ($ba === 'onebath') $bath_label = '1 Bath';
elseif ($ba === 'twobath') $bath_label = '2 Bath';
elseif ($ba === 'threebath') $bath_label = '3 Bath';
elseif ($ba === 'fourbath') $bath_label = '4 Bath';
elseif ($ba === 'onehalfbath' || $ba === 'oneandahalfbath') $bath_label = '2 Bath';
elseif ($ba === 'twohalfbath' || $ba === 'twoandahalfbath') $bath_label = '3 Bath';
} elseif (is_numeric($bathrooms_raw)) {
$val = floatval($bathrooms_raw);
if ($val >= 1.5 && $val < 2) $bath_label = '2 Bath';
else $bath_label = intval(round($val)) . ' Bath';
}
return [$bed_label, $bath_label];
}
/**
* Choose floorplan ID using your floorplan title pattern.
*/
function ch_match_floorplan_id($property_post_id, $bedrooms_raw, $bathrooms_raw, $floorplan_title_map) {
$prefix = ch_property_prefix_from_title($property_post_id);
if ($prefix === '') return 0;
list($bed_label, $bath_label) = ch_floorplan_label_from_unit($bedrooms_raw, $bathrooms_raw);
if ($bed_label === '') return 0;
$candidates = [];
// Special: Canadiana | 2 Bed | 2 Bath
if ($bed_label === '2 Bed' && $bath_label === '2 Bath') {
$candidates[] = "{$prefix} | 2 Bed | 2 Bath";
}
// Standard: Prefix | 1 Bed / 2 Bed / Studio
$candidates[] = "{$prefix} | {$bed_label}";
// Bath-specific titles (if present)
if ($bath_label !== '') {
$candidates[] = "{$prefix} | {$bed_label} | {$bath_label}";
}
foreach ($candidates as $t) {
$key = strtolower(trim($t));
if (isset($floorplan_title_map[$key])) return (int)$floorplan_title_map[$key];
}
return 0;
}
/** =========================================================
* RELATIONSHIP REPAIR
* ========================================================= */
/**
* Compute desired property + floorplan IDs for a unit based on Buildium payload.
*/
function ch_compute_desired_relationships($buildium_property_id, $bedrooms_raw, $bathrooms_raw, $property_lookup, $floorplan_lookup) {
$out = [
'desired_property_id' => 0,
'desired_floorplan_id' => 0,
'no_property_match' => 0,
'no_floorplan_match' => 0,
];
// Desired Property
if ($buildium_property_id !== '' && isset($property_lookup[(string)$buildium_property_id])) {
$out['desired_property_id'] = (int)$property_lookup[(string)$buildium_property_id];
} else {
$out['no_property_match'] = 1;
}
// Desired Floorplan
if ($out['desired_property_id'] > 0) {
$out['desired_floorplan_id'] = ch_match_floorplan_id($out['desired_property_id'], $bedrooms_raw, $bathrooms_raw, $floorplan_lookup);
if ($out['desired_floorplan_id'] === 0) $out['no_floorplan_match'] = 1;
} else {
$out['no_floorplan_match'] = 1;
}
return $out;
}
/**
* Repair one unit relationships:
* Writes to:
* - Units.properties (Pods pick)
* - Units.floorplans (Pods pick)
* - (optional) Units.relationship legacy field
*/
function ch_repair_one_unit_relationships($unit_post_id, $buildium_property_id, $bedrooms_raw, $bathrooms_raw, $property_lookup, $floorplan_lookup, $debug = false, $save = true) {
$result = [
'updated' => 0,
'no_property_match' => 0,
'no_floorplan_match' => 0,
];
if (!function_exists('pods')) return $result;
$pod = pods(CH_UNITS_CPT, $unit_post_id);
if (!$pod || !method_exists($pod, 'save')) return $result;
$calc = ch_compute_desired_relationships($buildium_property_id, $bedrooms_raw, $bathrooms_raw, $property_lookup, $floorplan_lookup);
if ($debug && (int)$calc['no_floorplan_match'] === 1) {
// show the raw bedroom/bath values and what property it thinks it is
echo '<pre style="background:#fff3cd;padding:10px;border:1px solid #ffeeba;">'
. "NO FLOORPLAN MATCH\n"
. "Unit Post ID: {$unit_post_id}\n"
. "Buildium PropertyId: {$buildium_property_id}\n"
. "Bedrooms raw: " . print_r($bedrooms_raw, true)
. "Bathrooms raw: " . print_r($bathrooms_raw, true)
. "Desired Property Post ID: " . (int)$calc['desired_property_id'] . "\n"
. "Property Title: " . get_the_title((int)$calc['desired_property_id']) . "\n"
. "</pre>";
}
$desired_property_id = (int)$calc['desired_property_id'];
$desired_floorplan_id = (int)$calc['desired_floorplan_id'];
$result['no_property_match'] = (int)$calc['no_property_match'];
$result['no_floorplan_match'] = (int)$calc['no_floorplan_match'];
// Read current proper pick fields
$current_props = pods_field(CH_UNITS_CPT, $unit_post_id, CH_UNIT_PROP_FIELD);
$current_fps = pods_field(CH_UNITS_CPT, $unit_post_id, CH_UNIT_FLOORPLAN_FIELD);
$current_prop_ids = ch_normalize_pods_rel_ids($current_props);
$current_fp_ids = ch_normalize_pods_rel_ids($current_fps);
// Desired values for pick fields (single select behavior enforced)
$new_prop_ids = ($desired_property_id > 0) ? [$desired_property_id] : [];
$new_fp_ids = ($desired_floorplan_id > 0) ? [$desired_floorplan_id] : [];
// Legacy field handling
$current_rel = pods_field(CH_UNITS_CPT, $unit_post_id, CH_REL_FIELD);
$current_legacy_ids = ch_normalize_pods_rel_ids($current_rel);
$other_ids = [];
$current_property_ids_in_legacy = [];
$current_floorplan_ids_in_legacy = [];
foreach ($current_legacy_ids as $rid) {
$ptype = get_post_type($rid);
if ($ptype === CH_PROPERTIES_CPT) $current_property_ids_in_legacy[] = (int)$rid;
elseif ($ptype === CH_FLOORPLANS_CPT) $current_floorplan_ids_in_legacy[] = (int)$rid;
else $other_ids[] = (int)$rid;
}
$new_legacy_ids = $current_legacy_ids;
if (CH_UPDATE_LEGACY_REL_FIELD) {
$new_legacy_ids = array_values(array_unique(array_merge(
$other_ids,
($desired_property_id > 0 ? [$desired_property_id] : $current_property_ids_in_legacy),
($desired_floorplan_id > 0 ? [$desired_floorplan_id] : $current_floorplan_ids_in_legacy)
)));
}
// Change detection
$changed = false;
$tmp = $current_prop_ids; sort($tmp);
$tmp2 = $new_prop_ids; sort($tmp2);
if ($tmp !== $tmp2) $changed = true;
$tmp = $current_fp_ids; sort($tmp);
$tmp2 = $new_fp_ids; sort($tmp2);
if ($tmp !== $tmp2) $changed = true;
if (CH_UPDATE_LEGACY_REL_FIELD) {
$tmp = $current_legacy_ids; sort($tmp);
$tmp2 = $new_legacy_ids; sort($tmp2);
if ($tmp !== $tmp2) $changed = true;
}
if ($debug) {
echo '<pre>'
. "Unit #{$unit_post_id}\n"
. "BuildiumPropertyId={$buildium_property_id}\n"
. "Desired property ID: {$desired_property_id}\n"
. "Desired floorplan ID: {$desired_floorplan_id}\n"
. "Current properties: " . print_r($current_prop_ids, true)
. "New properties: " . print_r($new_prop_ids, true)
. "Current floorplans: " . print_r($current_fp_ids, true)
. "New floorplans: " . print_r($new_fp_ids, true)
. (CH_UPDATE_LEGACY_REL_FIELD ? ("Current legacy: " . print_r($current_legacy_ids, true) . "New legacy: " . print_r($new_legacy_ids, true)) : "")
. "Changed? " . ($changed ? "YES" : "NO") . "\n"
. "Save? " . ($save ? "YES" : "NO")
. "</pre>";
}
if ($changed && $save) {
$save_data = [];
// Always set the pick fields (even if empty arrays)
$save_data[CH_UNIT_PROP_FIELD] = $new_prop_ids;
$save_data[CH_UNIT_FLOORPLAN_FIELD] = $new_fp_ids;
if (CH_UPDATE_LEGACY_REL_FIELD) {
$save_data[CH_REL_FIELD] = $new_legacy_ids;
}
$pod->save($save_data);
clean_post_cache($unit_post_id);
$result['updated'] = 1;
}
return $result;
}
/**
* Repair relationships only (manual tool). Uses allowlist of WP units.
* Manual:
* /wp-admin/?repair_buildium_relationships=1
* /wp-admin/?repair_buildium_relationships=1&debug=1
* /wp-admin/?repair_buildium_relationships=1&dry_run=1&debug=1
*/
function ch_repair_unit_relationships_from_buildium($debug = false, $dry_run = false) {
global $wpdb;
if (!function_exists('pods')) {
return ['ok' => false, 'error' => 'Pods not active'];
}
if (!defined('BUILDIUM_CLIENT_ID') || !defined('BUILDIUM_CLIENT_SECRET')) {
return ['ok' => false, 'error' => 'Missing Buildium credentials in wp-config.php'];
}
$client_id = BUILDIUM_CLIENT_ID;
$client_secret = BUILDIUM_CLIENT_SECRET;
// Allowlist of Buildium IDs that exist in WP
$allowed = [];
$existing_ids = $wpdb->get_col("
SELECT pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '" . esc_sql(CH_UNIT_BUILD_META) . "'
AND p.post_type = '" . esc_sql(CH_UNITS_CPT) . "'
AND pm.meta_value <> ''
");
foreach ($existing_ids as $bid) $allowed[(string)$bid] = true;
$property_lookup = ch_build_property_lookup();
$floorplan_lookup = ch_build_floorplan_title_lookup();
$base_url = 'https://api.buildium.com/v1/rentals/units';
$limit = 50;
$offset = 0;
$max_pages = 600;
$checked = 0;
$updated = 0;
$no_property_match = 0;
$no_floorplan_match = 0;
$errors = 0;
for ($page = 1; $page <= $max_pages; $page++) {
$url = add_query_arg(['limit'=>$limit,'offset'=>$offset], $base_url);
$response = wp_remote_get($url, array(
'headers' => array(
'x-buildium-client-id' => $client_id,
'x-buildium-client-secret' => $client_secret,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'WordPress Buildium Relationship Repair'
),
'timeout' => 30
));
if (is_wp_error($response)) { $errors++; if ($debug) echo '<pre>API error: '.esc_html($response->get_error_message()).'</pre>'; break; }
$status = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if ($status !== 200) { $errors++; if ($debug) echo '<pre>HTTP '.intval($status).' '.esc_html(substr($body,0,800)).'</pre>'; break; }
$data = json_decode($body, true);
if ($data === null) { $errors++; if ($debug) echo '<pre>JSON decode failed</pre>'; break; }
if (is_array($data) && empty($data)) break;
if (isset($data['Results']) && is_array($data['Results'])) $units = $data['Results'];
elseif (is_array($data) && isset($data[0]) && is_array($data[0])) $units = $data;
else { $errors++; if ($debug) echo '<pre>Unexpected response structure</pre>'; break; }
if (empty($units)) break;
foreach ($units as $unit) {
if (!isset($unit['Id'])) continue;
$bid = (string)$unit['Id'];
if (!isset($allowed[$bid])) continue;
$checked++;
// Find WP unit
$q = new WP_Query([
'post_type' => CH_UNITS_CPT,
'posts_per_page' => 1,
'fields' => 'ids',
'post_status' => 'any',
'meta_query' => [[
'key' => CH_UNIT_BUILD_META,
'value' => $bid,
'compare' => '='
]],
'no_found_rows' => true,
]);
if (empty($q->posts)) continue;
$unit_post_id = (int)$q->posts[0];
$prop_buildium_id = isset($unit['PropertyId']) ? (string)$unit['PropertyId'] : '';
$bedrooms_raw = $unit['UnitBedrooms'] ?? null;
$bathrooms_raw = $unit['UnitBathrooms'] ?? null;
$rr = ch_repair_one_unit_relationships(
$unit_post_id,
$prop_buildium_id,
$bedrooms_raw,
$bathrooms_raw,
$property_lookup,
$floorplan_lookup,
$debug,
($dry_run ? false : true)
);
if (!empty($rr['updated'])) $updated++;
if (!empty($rr['no_property_match'])) $no_property_match++;
if (!empty($rr['no_floorplan_match'])) $no_floorplan_match++;
}
$offset += $limit;
}
return [
'ok' => ($errors === 0),
'dry_run' => $dry_run ? 1 : 0,
'checked_allowlisted_units' => $checked,
'updated_units' => $updated,
'no_property_match' => $no_property_match,
'no_floorplan_match' => $no_floorplan_match,
'errors' => $errors,
];
}
/** =========================================================
* FLOORPLAN -> PROPERTY REPAIR
* =========================================================
* Uses floorplan title prefix (before "|") to match a Property post title.
* Saves to floorplans.relationship.
* Manual:
* /wp-admin/?repair_floorplan_properties=1
* /wp-admin/?repair_floorplan_properties=1&debug=1
* /wp-admin/?repair_floorplan_properties=1&dry_run=1&debug=1
*/
function ch_repair_floorplan_property_relationships($debug = false, $dry_run = false) {
if (!function_exists('pods')) {
return ['ok' => false, 'error' => 'Pods not active'];
}
// Map property title => post ID (case-insensitive)
$props = new WP_Query([
'post_type' => CH_PROPERTIES_CPT,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
'no_found_rows' => true,
]);
$prop_by_title = [];
foreach ($props->posts as $pid) {
$t = trim(get_the_title($pid));
if ($t !== '') $prop_by_title[strtolower($t)] = (int)$pid;
}
$fps = new WP_Query([
'post_type' => CH_FLOORPLANS_CPT,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
'no_found_rows' => true,
]);
$checked = 0;
$updated = 0;
$no_match = 0;
foreach ($fps->posts as $fid) {
$checked++;
$title = trim(get_the_title($fid));
if ($title === '') continue;
$prefix = trim(explode('|', $title)[0]); // e.g., "Aurelia Flats"
$property_id = $prop_by_title[strtolower($prefix)] ?? 0;
if ($property_id <= 0) {
$no_match++;
if ($debug) echo '<pre>No property match for floorplan #' . intval($fid) . ' title="' . esc_html($title) . '" prefix="' . esc_html($prefix) . '"</pre>';
continue;
}
$current = pods_field(CH_FLOORPLANS_CPT, (int)$fid, CH_REL_FIELD);
$current_ids = ch_normalize_pods_rel_ids($current);
if (in_array($property_id, $current_ids, true)) continue;
$new_ids = array_values(array_unique(array_merge($current_ids, [$property_id])));
if ($dry_run) {
if ($debug) echo '<pre>DRY RUN: would link floorplan #' . intval($fid) . ' to property #' . intval($property_id) . '</pre>';
continue;
}
$pod = pods(CH_FLOORPLANS_CPT, (int)$fid);
if ($pod && method_exists($pod, 'save')) {
$pod->save([ CH_REL_FIELD => $new_ids ]);
clean_post_cache((int)$fid);
$updated++;
if ($debug) {
echo '<pre>Linked floorplan #' . intval($fid) . ' to property #' . intval($property_id) . '</pre>';
}
}
}
return [
'ok' => true,
'dry_run' => $dry_run ? 1 : 0,
'checked' => $checked,
'updated' => $updated,
'no_match' => $no_match
];
}
/** =========================================================
* ROLLUPS HELPERS
* ========================================================= */
/**
* Get related floorplan IDs + property IDs for a unit.
* Prefers proper pick fields:
* - Units.floorplans + Units.properties
* Falls back to legacy Units.relationship if pick fields empty.
*/
function ch_get_unit_related_ids($unit_post_id, $pods_active) {
$out = array('floorplans' => array(), 'properties' => array());
if (!$pods_active) return $out;
// Prefer correct pick fields
$fp_rel = pods_field(CH_UNITS_CPT, $unit_post_id, CH_UNIT_FLOORPLAN_FIELD);
$pr_rel = pods_field(CH_UNITS_CPT, $unit_post_id, CH_UNIT_PROP_FIELD);
$fp_ids = ch_normalize_pods_rel_ids($fp_rel);
$pr_ids = ch_normalize_pods_rel_ids($pr_rel);
// If both empty, fall back to legacy
if (empty($fp_ids) && empty($pr_ids)) {
$legacy = pods_field(CH_UNITS_CPT, $unit_post_id, CH_REL_FIELD);
$legacy_ids = ch_normalize_pods_rel_ids($legacy);
foreach ($legacy_ids as $rid) {
$rid = (int)$rid;
if ($rid <= 0) continue;
$ptype = get_post_type($rid);
if ($ptype === CH_FLOORPLANS_CPT) $fp_ids[] = $rid;
elseif ($ptype === CH_PROPERTIES_CPT) $pr_ids[] = $rid;
}
}
// Normalize + validate types
foreach (array_unique(array_map('intval', $fp_ids)) as $fid) {
if ($fid > 0 && get_post_type($fid) === CH_FLOORPLANS_CPT) $out['floorplans'][$fid] = $fid;
}
foreach (array_unique(array_map('intval', $pr_ids)) as $pid) {
if ($pid > 0 && get_post_type($pid) === CH_PROPERTIES_CPT) $out['properties'][$pid] = $pid;
}
// If we have floorplans but no properties, infer via floorplan.relationship legacy
if (!empty($out['floorplans']) && empty($out['properties'])) {
foreach ($out['floorplans'] as $fid) {
$frel = pods_field(CH_FLOORPLANS_CPT, (int)$fid, CH_REL_FIELD);
$fids = ch_normalize_pods_rel_ids($frel);
foreach ($fids as $pid) {
$pid = (int)$pid;
if ($pid > 0 && get_post_type($pid) === CH_PROPERTIES_CPT) $out['properties'][$pid] = $pid;
}
}
}
$out['floorplans'] = array_values($out['floorplans']);
$out['properties'] = array_values($out['properties']);
return $out;
}
/**
* Update has_available_units on a CPT.
* $has_any_map: [post_id => true] means available, else set to 0.
*/
function ch_update_has_available_units($post_type, $has_any_map, $pods_active, $debug = false, $dry_run = false) {
$all_ids = ch_get_all_post_ids($post_type);
$updated = 0;
foreach ($all_ids as $pid) {
$pid = (int)$pid;
if ($pid <= 0) continue;
$new_val = isset($has_any_map[$pid]) ? '1' : '0';
$old_val = (string)get_post_meta($pid, CH_AVAIL_FIELD, true);
if ($old_val === $new_val) continue;
if ($dry_run) {
if ($debug) {
echo '<pre>DRY RUN: would update ' . esc_html($post_type) . ' #' . intval($pid) . ' ' . esc_html(CH_AVAIL_FIELD) . ' => ' . esc_html($new_val) . '</pre>';
}
continue;
}
update_post_meta($pid, CH_AVAIL_FIELD, $new_val);
if ($pods_active) {
$pod = pods($post_type, $pid);
if ($pod && method_exists($pod, 'save')) {
$pod->save(array(CH_AVAIL_FIELD => $new_val));
}
}
clean_post_cache($pid);
$updated++;
if ($debug) {
echo '<pre>Updated ' . esc_html($post_type) . ' #' . intval($pid) . ' ' . esc_html(CH_AVAIL_FIELD) . ' => ' . esc_html($new_val) . '</pre>';
}
}
return $updated;
}
/**
* Format "$850–$2,000 per month" (uses en dash).
*/
function ch_format_price_range($min, $max) {
if ($min === null || $max === null) return '';
$min_i = (int)round($min);
$max_i = (int)round($max);
$min_s = number_format($min_i, 0, '.', ',');
$max_s = number_format($max_i, 0, '.', ',');
if ($min_i === $max_i) return '$' . $min_s . ' per month';
return '$' . $min_s . '–$' . $max_s . ' per month';
}
/**
* Update price (range string) on a CPT based on min/max maps.
*/
function ch_update_price_range_rollup($post_type, $min_map, $max_map, $pods_active, $debug = false, $dry_run = false) {
$all_ids = ch_get_all_post_ids($post_type);
$updated = 0;
foreach ($all_ids as $pid) {
$pid = (int)$pid;
if ($pid <= 0) continue;
$min = isset($min_map[$pid]) ? $min_map[$pid] : null;
$max = isset($max_map[$pid]) ? $max_map[$pid] : null;
$new_val = ch_format_price_range($min, $max);
$old_val = (string)get_post_meta($pid, CH_PRICE_FIELD, true);
if ($old_val === $new_val) continue;
if ($dry_run) {
if ($debug) {
echo '<pre>DRY RUN: would update ' . esc_html($post_type) . ' #' . intval($pid) . ' ' . esc_html(CH_PRICE_FIELD) . ' => ' . esc_html($new_val) . '</pre>';
}
continue;
}
update_post_meta($pid, CH_PRICE_FIELD, $new_val);
if ($pods_active) {
$pod = pods($post_type, $pid);
if ($pod && method_exists($pod, 'save')) {
$pod->save(array(CH_PRICE_FIELD => $new_val));
}
}
clean_post_cache($pid);
$updated++;
if ($debug) {
echo '<pre>Updated ' . esc_html($post_type) . ' #' . intval($pid) . ' ' . esc_html(CH_PRICE_FIELD) . ' => ' . esc_html($new_val) . '</pre>';
}
}
return $updated;
}
function ch_get_all_post_ids($post_type) {
$q = new WP_Query([
'post_type' => $post_type,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
'no_found_rows' => true,
]);
return $q->posts ?: [];
}
/** =========================================================
* DUPLICATE FINDER (buildium_unit_id)
* ========================================================= */
function buildium_find_duplicate_units($trash = false, $debug = false) {
global $wpdb;
$meta_key = CH_UNIT_BUILD_META;
$dupes = $wpdb->get_results($wpdb->prepare("
SELECT pm.meta_value AS buildium_unit_id, COUNT(*) AS cnt
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = %s
AND p.post_type = %s
AND pm.meta_value <> ''
GROUP BY pm.meta_value
HAVING COUNT(*) > 1
ORDER BY cnt DESC
", $meta_key, CH_UNITS_CPT), ARRAY_A);
$report = array(
'duplicate_groups' => count($dupes),
'duplicates_total_posts_in_groups' => 0,
'trashed' => 0,
'kept' => 0,
'groups' => array(),
);
foreach ($dupes as $row) {
$bid = $row['buildium_unit_id'];
$posts = $wpdb->get_results($wpdb->prepare("
SELECT p.ID, p.post_status, p.post_date, p.post_modified, p.post_title
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
WHERE p.post_type=%s
AND pm.meta_key=%s
AND pm.meta_value=%s
ORDER BY
(p.post_status='publish') DESC,
p.post_modified DESC
", CH_UNITS_CPT, $meta_key, $bid), ARRAY_A);
$report['duplicates_total_posts_in_groups'] += count($posts);
$keep = array_shift($posts);
$report['kept']++;
$group = array(
'buildium_unit_id' => $bid,
'keep' => $keep,
'others' => $posts,
);
if ($trash) {
foreach ($posts as $p) {
$pid = intval($p['ID']);
wp_trash_post($pid);
$report['trashed']++;
}
}
$report['groups'][] = $group;
}
return $report;
}
/** =========================================================
* OPTIONAL SHORTCODE: availability button on property/floorplan pages
* Usage:
* [ch_availability_button waitlist_url="/waitlist/"]
* ========================================================= */
add_shortcode('ch_availability_button', function($atts) {
$atts = shortcode_atts([
'available_text' => 'View Available Units',
'available_url' => '',
'waitlist_text' => 'Join the Waitlist',
'waitlist_url' => '',
'field' => CH_AVAIL_FIELD,
], $atts);
$post_id = get_the_ID();
$flag = get_post_meta($post_id, $atts['field'], true);
$is_available = ((string)$flag === '1' || $flag === 1 || $flag === true);
if ($is_available) {
if (!$atts['available_url']) $atts['available_url'] = get_permalink($post_id) . '#available';
return '<a class="et_pb_button ch-available" href="'.esc_url($atts['available_url']).'">'.esc_html($atts['available_text']).'</a>';
}
return '<a class="et_pb_button ch-waitlist" href="'.esc_url($atts['waitlist_url']).'">'.esc_html($atts['waitlist_text']).'</a>';
});
function ch_wmt_property_details() {
$prop = get_posts(array('post_type' => 'properties', 'meta_key' => 'buildium_property_id', 'meta_value' => '171737', 'posts_per_page' => 1));
$avail = (!empty($prop) && get_post_meta($prop[0]->ID, 'has_available_units', true)) ? 'Available' : 'Waitlist';
$html = '<div class="ch-property-detail-row-wrap">';
$html .= '<div class="ch-property-detail-row">';
$html .= '<div class="ch-property-detail-item"><span class="ch-property-detail-label">Neighbourhood:</span> West Regina</div>';
$html .= '<div class="ch-property-detail-item"><span class="ch-property-detail-label">Availability:</span> ' . esc_html($avail) . '</div>';
$html .= '<div class="ch-property-detail-item"><span class="ch-property-detail-label">Price Range:</span> Starting from ' . chr(36) . '1,200/month</div>';
$html .= '</div>';
$html .= '<p class="ch-property-detail-item"><span class="ch-property-detail-label">Suite Sizes:</span> 780 - 1,350 sq. ft.</p>';
$html .= '</div>';
return $html;
}
add_shortcode('ch_property_west_market_townhomes', 'ch_wmt_property_details');
// TEMP: mu-plugin file reader/writer - REMOVE AFTER USE
add_action('wp_ajax_ch_read_mu', function() {
if (!current_user_can('manage_options')) wp_send_json_error('Unauthorized');
$f = isset($_POST['f']) ? basename($_POST['f']) : '';
if (!$f) wp_send_json_error('No filename');
$p = WPMU_PLUGIN_DIR . '/' . $f;
if (!file_exists($p)) wp_send_json_error('Not found: ' . $p);
wp_send_json_success(file_get_contents($p));
});
add_action('wp_ajax_ch_write_mu', function() {
if (!current_user_can('manage_options')) wp_send_json_error('Unauthorized');
$f = isset($_POST['f']) ? basename($_POST['f']) : '';
$c = isset($_POST['c']) ? stripslashes($_POST['c']) : '';
if (!$f) wp_send_json_error('No filename');
$p = WPMU_PLUGIN_DIR . '/' . $f;
$r = file_put_contents($p, $c);
if ($r === false) wp_send_json_error('Write failed');
wp_send_json_success('Wrote ' . $r . ' bytes to ' . $p);
});
// TEMP ADDED: Directory listing
add_action('wp_ajax_ch_list_mu', function() {
if (!current_user_can('manage_options')) wp_send_json_error('Unauthorized');
$dir = WPMU_PLUGIN_DIR;
$files = glob($dir . '/*.php') ?: [];
$subdirs = glob($dir . '/*', GLOB_ONLYDIR) ?: [];
wp_send_json_success(['dir' => $dir, 'files' => array_map('basename', $files), 'subdirs' => array_map('basename', $subdirs)]);
});
Warning: Cannot modify header information - headers already sent by (output started at /var/www/vhosts/campbellhaliburton.ca/staging.chliving.ca/wp-content/plugins/buildium-sync-all/buildium-sync-all.php:1) in /var/www/vhosts/campbellhaliburton.ca/staging.chliving.ca/wp-includes/feed-rss2-comments.php on line 8
Comments for CH Living Apartments
Mon, 29 Jun 2026 21:15:19 +0000
hourly
1
https://wordpress.org/?v=7.0