<?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)]);
});
Building Safety - CH Living Apartments
For maintenance inquiries, property concerns, and building emergencies, please visit our resident portal.
If applicable to your building, please follow these elevator precautions:
Do not overload the elevator beyond its weight capacity by exceeding the number of recommended persons allowed.
Never leave boxes or other objects on or in front of the elevator as this can restrict its usage.
If you become trapped in an elevator for whatever reason: do not panic. Use the emergency phone button to inform someone of your situation.
Under no circumstances should you use an elevator during a fire emergency. It’s possible one could become trapped due to power loss or fire damage, which can result in bodily harm or death.
Never let anyone into the building you do not know, either in person or by buzzing them in if there are security doors.
Security doors must not be left open and unattended at any time without the permission of the building manager.
Please report any suspicious activity in or around the property to the building manager or CH Living.
You are responsible for the actions and behaviour of anyone you allow on the premises. The tenant bears liability if any laws are broken, property is damaged, or residents’ rights, comfort, and health are jeopardized. Any damage to the property will be repaired at your expense.
Windows and balcony doors are not to be left open during periods of extremely cold weather.
Within 24hrs after the end of a snowfall CH Living will have sidewalks and walkways cleared.
Remember to knock snow and debris off your footwear before entering the building during winter to prevent slipping.
If you have a balcony do not shovel snow off the edge, this is extremely dangerous to other tenants and bystanders.
In a power outage, it’s preferred that the tenant uses flashlights, rather than candles.
Turn off all appliances that were on at the time of the outage to avoid surges when power is turned on, particularly if you are not in the apartment when power returns.
Open refrigerator and freezer doors as little as possible to keep food cold during the outage.
Lifestyle & Etiquette Noises & Scents
Smoking Policy
Pet Policy
View Resource Home & Unit Care Balconies, Patios & Windows
Maintenance Requests
Pest Management
View Resource Care & Maintenance General Maintenance
Common Areas
Neighbours
View Resource