<?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.php on line 8
Resources Archive - CH Living Apartments Thu, 12 Mar 2026 17:19:18 +0000 en-US hourly 1 https://wordpress.org/?v=7.0 Building Safety https://staging.chliving.ca/resources/building-safety/ Thu, 12 Mar 2026 17:19:18 +0000 https://staging.chliving.ca/?post_type=resources&p=8363 The post Building Safety appeared first on CH Living Apartments.

]]>
The post Building Safety appeared first on CH Living Apartments.

]]>
Fire Safety https://staging.chliving.ca/resources/fire-safety/ Thu, 12 Mar 2026 17:18:57 +0000 https://staging.chliving.ca/?post_type=resources&p=8361 The post Fire Safety appeared first on CH Living Apartments.

]]>
The post Fire Safety appeared first on CH Living Apartments.

]]>
Unit Care https://staging.chliving.ca/resources/unit-care/ Thu, 12 Mar 2026 17:04:22 +0000 https://staging.chliving.ca/?post_type=resources&p=8351 The post Unit Care appeared first on CH Living Apartments.

]]>
The post Unit Care appeared first on CH Living Apartments.

]]>
Tips for Decorating Your New Apartment https://staging.chliving.ca/resources/tips-for-decorating-your-new-apartment/ Wed, 11 Mar 2026 21:34:56 +0000 https://staging.chliving.ca/?post_type=resources&p=8212 Moving into a new apartment is an exciting chapter, whether it’s your first or fifth time. However, decorating a new space can often feel overwhelming. To help you create a stylish and functional home, here are some tips from the CH Living team: 1. Add a Colourful Rug A fun rug can be a game-changer […]

The post Tips for Decorating Your New Apartment appeared first on CH Living Apartments.

]]>
Moving into a new apartment is an exciting chapter, whether it’s your first or fifth time. However, decorating a new space can often feel overwhelming. To help you create a stylish and functional home, here are some tips from the CH Living team:

1. Add a Colourful Rug

A fun rug can be a game-changer for any room. It can define a space and put some personality into your decor. We recommend choosing a rug that can compliment your furniture and overall color scheme!

 

2. Create a Small Breakfast Nook

If space is limited, a cozy breakfast nook can be a great addition. Utilize a corner of your kitchen or living area with a small table and chairs. You could even use a built-in bench. It’s a perfect spot for morning coffee or casual meals without taking up too much space.

 

3. Use Mirrors

Mirrors are a decorator’s secret weapon for making small spaces feel larger and more open. Place mirrors strategically to reflect light and create the illusion of more space. A large mirror in the living room or bedroom can make a significant difference.

 

4. Utilize Bathroom Storage

Bathrooms are often one of the most challenging spaces to organize. Invest in over-the-toilet storage, floating shelves, or multi-functional cabinets to keep your essentials tidy and within reach. This can help you maximize every inch of space.

 

5. Use Curtains to Create An Entryway

Curtains aren’t just for windows! Use them to create a stylish entryway or to separate spaces within your apartment. Hanging curtains can make your apartment feel more personalized and cozy.

6. Incorporate Multi-Functional Furniture

Opt for furniture that serves more than one purpose. A sofa bed, a coffee table with storage, or a fold-out desk can be lifesavers in a small apartment. These pieces help you make the most of your space without compromising on style or functionality.

 

The post Tips for Decorating Your New Apartment appeared first on CH Living Apartments.

]]>
Credit Checks https://staging.chliving.ca/resources/credit-checks/ Wed, 11 Mar 2026 21:34:19 +0000 https://staging.chliving.ca/?post_type=resources&p=8210 Why do rental companies or landlords do a credit check? Credit checks are performed to ensure that a prospective tenant can afford the rental fee. It’s important for both the tenant and the landlord. Financially overcommitting puts undue stress on the tenant, while recouping rent is how the landlord ensures their investment is viable and […]

The post Credit Checks appeared first on CH Living Apartments.

]]>
Why do rental companies or landlords do a credit check?

Credit checks are performed to ensure that a prospective tenant can afford the rental fee. It’s important for both the tenant and the landlord. Financially overcommitting puts undue stress on the tenant, while recouping rent is how the landlord ensures their investment is viable and funds are available to maintain properties.

 

Will this credit check hurt my credit score?

At CH Living, we do soft credit checks rather than hard credit checks. What is the difference? Soft inquiries are when a rental company does a background check for pre-approval purposes. Hard inquiries are when you apply for credit from a financial institution. This may take the form of a loan, credit card, or mortgage.

Soft inquiries DO NOT damage your score.

CH Living’s rental department only performs soft credit checks, so prospective tenants can be verified without the individual having to worry about their credit score being affected.

 

 

 What if I have poor credit?

There are risk factors involved for landlords and rental companies when an individual has a poor credit rating. An applicant’s credit history is an indicator as to whether the individual has the financial means to rent a property. However; once the credit rating is known, the rental team can decide whether there were factors that contributed to the poor rating and if steps have been taken to rectify the situation. So, a poor credit rating doesn’t necessarily mean that a tenant will be excluded from a rental opportunity. There are also other factors that are taken into account when CH Living’s rental team assesses a potential client. This can include checking an applicant’s rental references with previous landlords.

“Life happens and many landlords understand that. If you feel comfortable doing so, share your situation with the potential landlord, giving a brief explanation of why your credit is poor (e.g. relationship breakdown), but assure him or her that you will pay the rent. Be careful to not disclose too much personal information,” Community Services from the Region of Waterloo.

How do I know if I have good credit?

The Canada Revenue Agency (CRA) provides information on services that provide free online credit scores, plus additional useful information on credit reports including ways to improve your credit score. A simple way to build your credit score is to make monthly rent payments on time. This demonstrates the responsibility to pay rent consistently.

 

The CH Living team is always available to answer questions related to the rental process during regular business hours.

This blog is from the view of CH Living. Please note that various landlords and rental companies may conduct hard credit checks or adopt different procedures throughout the rental process.

The post Credit Checks appeared first on CH Living Apartments.

]]>
Walkthrough Inspection Guide https://staging.chliving.ca/resources/walkthrough-inspection-guide/ Wed, 11 Mar 2026 21:33:46 +0000 https://staging.chliving.ca/?post_type=resources&p=8208 1. Plan Your Budget The first thing you should do is pick a budget and stick to it. Choose an apartment where rent costs a third of your monthly income, at most. Resist the temptation to splurge on new accessories and appliances while you’re getting settled in. Take what you can from your previous home, […]

The post Walkthrough Inspection Guide appeared first on CH Living Apartments.

]]>
1. Plan Your Budget

The first thing you should do is pick a budget and stick to it. Choose an apartment where rent costs a third of your monthly income, at most. Resist the temptation to splurge on new accessories and appliances while you’re getting settled in. Take what you can from your previous home, and let the rest come slowly. Keep an eye on your monthly spending, and always be sure to live within your means.

 

2. Be Courteous

Sharing your apartment building with other renters is nothing like sharing a room with your younger sibling. The people in your building may live very different lifestyles than you do, so cranking up the volume on your music after midnight may be disturbing the person next door who has to wake up for work in a few hours. Be respectful of your neighbours, and you’ll see the same level of respect being sent back to you!

 

3. Get Renters Insurance

Renters insurance protects you from life’s unexpected accidents. Fires or floods can do a lot of damage in a little time, and without renters insurance you may find yourself suddenly having to replace all of your belongings on your own. Plus, renters insurance helps protect you if any of these accidents happen to be started in your apartment! It’s a small monthly expense that may end up saving you loads of money in the future.

The post Walkthrough Inspection Guide appeared first on CH Living Apartments.

]]>
Move-In Guide https://staging.chliving.ca/resources/move-in-guide/ Wed, 11 Mar 2026 21:33:13 +0000 https://staging.chliving.ca/?post_type=resources&p=8206 You finally snagged that dream apartment (or home) you always wanted and are ready move in. While excited, it’s a bit overwhelming. For a successful move, there is a lot of preparation involved. Therefore, the CH Living rental team are going to share some helpful tips to prepare for your next move-in day!   Before […]

The post Move-In Guide appeared first on CH Living Apartments.

]]>
You finally snagged that dream apartment (or home) you always wanted and are ready move in. While excited, it’s a bit overwhelming. For a successful move, there is a lot of preparation involved. Therefore, the CH Living rental team are going to share some helpful tips to prepare for your next move-in day!

 

Before Move-In

 

1. Transfer Your Utilites Ahead of Time

Transfer or schedule utilities so they are effective on move-in day. This will ensure that you immediately have all the comforts of home, including wi-fi, internet, heat, power, and water, depending on what is included with your rent.

2. Update Addresses

Update your address effective at least a week before your move. This is particularly important if you are expecting either sensitive or urgent mail. Remember to also update your address with your bank, medical office(s), insurance office, SGI, eHealth, and employer.

3. Get Tenant Insurance

Rental companies require people who rent to have tenant insurance to protect their personal belongings from fire, theft, and water and sewer back-up. Arrange your insurance prior to moving.

4. Plan Movers

Whether you hire professional movers or enlist in some friends with the promise of pizza and drinks – make sure to confirm your movers at least one month in advance!

5. Pre-Move Decluttering

Make sure to dispose of any items you do not use, need, or want. ALWAYS inspect for sign of pests and dispose or treat the belongings before moving to a new location.

6. Label Boxes and Pack in Advance

To be able to immediately find what’s important on move day (toilet paper, lightbulbs, etc.), label your boxes by both room and importance! We also recommend packing gradually to reduce stress. Begin with the belongings you don’t use daily, adding more as you near moving day.

7. Coordinate Keys and Inspection

Arrange to have your keys before your movers arrive. If possible, coordinate a walkthrough inspection before move-in day.

 

Move-In Day

 

1. Transporting Valuables and Pets

Always prepare in advance the transportation of your furry friends and valuables. The CH Living team recommends moving pets, plants, and valuables in a separate vehicle to ensure their safety.

2. Take Measurements of Your New Place

Take measurements of all rooms before buying anything new. This will not only help you decide what you need for the space but also assist you in deciding if there are items you may need to part ways with!

3. Unpack Essential Items First

Make sure to unpack and set up your most essential items first. For example, unpack dishes, kitchen utensils and garbage bags, as you will need these immediately. We also recommend to prioritize setting up the most important furniture items first (e.g. bed). This is just in case you are too tired by the end of your move to set it up.  

CH Living’s Move-In Essentials

 

  1. A stereo to play your favourite music while you move!
  2. Cleaning products – rubber gloves, broom mop, pail, vacuum, garbage and recycling bags, and biodegradable plates and serviettes.
  3. Hand sanitizer, soap, and toilet paper.
  4. Hex key (Allen wrench), gloves, and a screwdriver for setting up your items.
  5. An X-ACTO knife and scissors for dismantling boxes.
  6. Shower rod, hooks, and curtain.
  7. Champagne, wine, or beer glasses for after your move!

The CH Living team is always available to answer questions related to the rental process during regular business hours. This blog is from the view of CH Living. Please note that various landlords and rental companies may have different opinions on move-in days and throughout the rental process.

The post Move-In Guide appeared first on CH Living Apartments.

]]>
Amenities https://staging.chliving.ca/resources/amenities/ Wed, 11 Mar 2026 21:30:43 +0000 https://staging.chliving.ca/?post_type=resources&p=8201 Recreation Rooms Recreation rooms are available to all residents and their guests. However, please respect the building and your fellow neighbours. Please keep to your booked time. Be sure to clean the area after use and dispose of any garbage. An event should be booked in advance by either notifying the building manager or recording […]

The post Amenities appeared first on CH Living Apartments.

]]>

Recreation Rooms

Recreation rooms are available to all residents and their guests. However, please respect the building and your fellow neighbours.

  • Please keep to your booked time.
  • Be sure to clean the area after use and dispose of any garbage.
  • An event should be booked in advance by either notifying the building manager or recording the time on the board provided, as available.

Gyms & Saunas

  • Gyms and saunas are available to residents 24/7. Use the facilities at your own risk. CH Living is not responsible for injuries caused by the use of
    equipment
  • Ensure that the door is locked upon leaving and disinfect surfaces both before and after usage. Please consult a doctor before beginning any exercise program.
  • If you believe the sauna or gym equipment requires maintenance or appears broken, do not use it. Contact your building manager immediately.

Laundry Facilities

  • Laundry equipment is for the exclusive use of residents and should only be used at the scheduled times.
  • Be considerate of others using the laundry facilities: wash off any spilled detergent and wipe out the washer drum.
  • Always be sure to remove lint from the dryer filter and throw it in the garbage.
  • Do not throw empty laundry detergent bottles in the garbage; take them to the building’s recycling or garbage container.

The post Amenities appeared first on CH Living Apartments.

]]>
Lifestyle & Etiquette https://staging.chliving.ca/resources/lifestyle-etiquette/ Wed, 11 Mar 2026 21:28:05 +0000 https://staging.chliving.ca/?post_type=resources&p=8196 Noises & Scents A notice will be given in the event of a noise complaint. If the situation continues, a notice of eviction may be served. Please be considerate of your neighbors by refraining from loud conversations or other noise (such as stereos, TV, musical instruments, vacuum cleaners, etc.) between 10 p.m. and 8 a.m. […]

The post Lifestyle & Etiquette appeared first on CH Living Apartments.

]]>

Noises & Scents

  • A notice will be given in the event of a noise complaint. If the situation continues, a notice of eviction may be served.
  • Please be considerate of your neighbors by refraining from loud conversations or other noise (such as stereos, TV, musical instruments, vacuum cleaners, etc.) between 10 p.m. and 8 a.m. Additionally, no loud or unusual noises are permitted in the hallways or elevators.
  • Using tools for pounding, drilling, sawing, grinding, etc., is strictly prohibited.
  • Some individuals have sensitivities to strong scents. While certain aromas can be pleasant, they might be overwhelming to others. Be aware and considerate in common areas where cooking smells might linger. Cooking food with milder smells and using proper ventilation can help create a more comfortable environment for everyone!
  • Do not run in hallways or your suite, as the sound will carry for several floors.

Pet Maintenance

CARE & MAINTENANCE

Pets of any description may only be kept on the property with CH Living’s prior written consent. If consent is not provided, a charge of $25 per day for each day the pet was kept will be incurred and deducted from your security deposit.

  • You may apply to keep a pet on the property. If approved, a pet agreement will be provided.
  • If you are approved to have a pet in your unit, you are responsible for ensuring that it is leashed and does not roam freely in the hallways.
  • The tenant is responsible for ensuring their pet remains quiet, as well as keeping the unit free from animal fur, waste, and unpleasant odours.

Smoking Policy

CH Living’s buildings are 100% smoke-free. Smoking or vaping is strictly prohibited in the elevators, all hallways, staircases, and all common areas, including your suite and balcony!

  • When smoking outside, please remain at least 30 ft. or 10 m from building entrances, windows, and openings. Always fully extinguish and safely dispose of smoking materials.
  • Please be advised that if you smoke or permit smoking in your suite, you will be given a written warning or notice to vacate the property.
  • If found to be smoking, tenant will be charged the cost of repainting, professionally cleaning, and any other costs associated with eradicating smells and stains in the unit or building.

The post Lifestyle & Etiquette appeared first on CH Living Apartments.

]]>
Care & Maintenance https://staging.chliving.ca/resources/care-maintenance/ Wed, 11 Mar 2026 21:19:35 +0000 https://staging.chliving.ca/?post_type=resources&p=8191 General This section provides insights into maintaining your suite and the CH Living building: Without prior written consent from CH Living, do not alter, amend, improve, or change your suite. This includes the use of paint and wallpaper. Any accident or damage to the premises should be reported to the building manager immediately. Garbage must […]

The post Care & Maintenance appeared first on CH Living Apartments.

]]>

General

This section provides insights into maintaining your suite and the CH Living building:

  • Without prior written consent from CH Living, do not alter, amend, improve, or change your suite. This includes the use of paint and wallpaper.
  • Any accident or damage to the premises should be reported to the building manager immediately.
  • Garbage must be removed from apartments, hallways, common areas, or balconies.
  • Do not flush or rinse garbage, including peels, grease, coffee grounds, disposable diapers, facial tissues, sanitary napkins or products, paper towels, disposable rubber gloves. This prevents clogging of drains or pipes.

Common Areas

Solicitation of goods or services is prohibited in common areas
shared by all tenants.

  • If eating or drinking in common areas, it is the responsibility of the tenant to clean up after themselves and their guests.
  • Ads may only be posted in designated areas if the building manager approves them.
  • Consider the schedule of other tenants. Their hours of work may vary from your own.
  • Appropriate attire is required in the lobby, elevators, hallways, or other common areas.
  • If there are challenges with your neighbours, please try talking to them calmly about your concerns to reach a resolution.
  • No personal items of any kind should be placed and left in the lobby, elevator(s), or hallways.
  • Respect parking policies and kindly request that guests adhere to the same guidelines as yourself.

Neighbours

  • Introducing yourself to new neighbours and greeting them in the elevator or lobby creates a pleasant community experience for all.
  • Consider the schedule of other tenants. Their hours of work may vary from your own. Keeping noise at a minimum and abiding by the rules of tenancy will foster healthy relationships with those who live in the building.
  • Respect parking policies and have guests do the same.
  • If there are challenges with your neighbours, please try talking to them calmly about your concerns to reach a resolution. If not possible, speak to your building manager.

The post Care & Maintenance appeared first on CH Living Apartments.

]]>