Snapshot: MLS sync fixes, image refresh, plugin/theme updates
MLS plugin fixes from this session: - Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls, causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL with ST_PointFromText so the spatial column is populated atomically. - Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by a property-level GET_LOCK so concurrent fetches share one API refresh. - Normalize WP_Error to null in mls_get_property_image() so callers can rely on the documented string|null contract. - Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm) actually filters correctly. - Incremental sync now looks back 10 minutes past the latest modification timestamp as a safety margin against missed records. - Smart sync exits silently (info-level, not warning) when a full sync is in progress. Operational: - New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync). - New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/ (/usr/local/bin/mls-image-cache-cap). - Logrotate config for wp-content/debug.log (2-day retention, daily rotation, delaycompress). Repo policy: - CLAUDE.md updated with explicit "commit everything except build artifacts" policy. - .gitignore: untrack runtime image caches and debug.log rotations. Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,25 @@ class MLS_Query {
|
||||
return "(street_number IS NULL OR (street_number != 'TBD' AND street_number NOT LIKE 'TBD %'))";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a WHERE clause for property_type, supporting comma-separated values.
|
||||
*
|
||||
* @param string $property_type Single type or comma-separated types
|
||||
* @param array &$where WHERE clause fragments
|
||||
* @param array &$values Prepared statement values
|
||||
*/
|
||||
private function build_property_type_clause($property_type, &$where, &$values) {
|
||||
$types = array_filter(array_map('trim', explode(',', $property_type)));
|
||||
if (count($types) === 1) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $types[0];
|
||||
} elseif (count($types) > 1) {
|
||||
$placeholders = implode(',', array_fill(0, count($types), '%s'));
|
||||
$where[] = "property_type IN ({$placeholders})";
|
||||
$values = array_merge($values, $types);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coordinates for a zip code from geo table
|
||||
*
|
||||
@@ -246,13 +265,20 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
if ($args['status']) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
if (is_array($args['status'])) {
|
||||
// Multiple statuses - use IN clause
|
||||
$placeholders = implode(',', array_fill(0, count($args['status']), '%s'));
|
||||
$where[] = "standard_status IN ({$placeholders})";
|
||||
$values = array_merge($values, $args['status']);
|
||||
} else {
|
||||
// Single status
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($args['property_type']) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
$this->build_property_type_clause($args['property_type'], $where, $values);
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
@@ -503,17 +529,16 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM {$table}";
|
||||
// Don't filter by status here - we filter AFTER normalization
|
||||
// because status is derived from MLS for linked properties
|
||||
$where = array("standard_status != 'Withdrawn'");
|
||||
$values = array();
|
||||
|
||||
if ($args['status']) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
}
|
||||
// Store requested status for post-normalization filtering
|
||||
$requested_status = $args['status'];
|
||||
|
||||
if ($args['property_type']) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
$this->build_property_type_clause($args['property_type'], $where, $values);
|
||||
}
|
||||
|
||||
if ($args['city']) {
|
||||
@@ -566,6 +591,31 @@ class MLS_Query {
|
||||
$values[] = $args['listing_id'];
|
||||
}
|
||||
|
||||
// Filter by agent MLS ID - find agent post with this MLS ID
|
||||
if (!empty($args['agent_mls_id'])) {
|
||||
$agent_query = new WP_Query(array(
|
||||
'post_type' => 'agent',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'agent_mls_id',
|
||||
'value' => $args['agent_mls_id'],
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
));
|
||||
if ($agent_query->have_posts()) {
|
||||
$where[] = 'list_agent_post_id = %d';
|
||||
$values[] = $agent_query->posts[0];
|
||||
} else {
|
||||
// No agent found with this MLS ID - return empty for manual properties
|
||||
return array();
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
if ($args['search']) {
|
||||
$search_term = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||
$where[] = '(full_address LIKE %s OR city LIKE %s OR public_remarks LIKE %s OR listing_id LIKE %s)';
|
||||
@@ -616,10 +666,22 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
// Normalize results to match MLS schema
|
||||
// This fetches the real status from MLS for linked properties
|
||||
foreach ($results as $key => $property) {
|
||||
$results[$key] = $this->normalize_manual_property($property);
|
||||
}
|
||||
|
||||
// Filter by status AFTER normalization (status is now MLS-derived)
|
||||
if ($requested_status) {
|
||||
$results = array_filter($results, function($property) use ($requested_status) {
|
||||
if (is_array($requested_status)) {
|
||||
return in_array($property->standard_status, $requested_status);
|
||||
}
|
||||
return $property->standard_status === $requested_status;
|
||||
});
|
||||
$results = array_values($results); // Re-index array
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -668,9 +730,99 @@ class MLS_Query {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if media records need refresh (for single property view)
|
||||
if ($property) {
|
||||
$this->ensure_media_records($property);
|
||||
}
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure media records exist and are fresh for a property
|
||||
*
|
||||
* Checks if:
|
||||
* 1. Media record count < photos_count (missing media records)
|
||||
* 2. Any media URLs are expired
|
||||
*
|
||||
* If either condition is true, refreshes media from the API.
|
||||
*
|
||||
* @param object $property Property object with listing_key, listing_id, photos_count
|
||||
*/
|
||||
private function ensure_media_records($property) {
|
||||
global $wpdb;
|
||||
|
||||
// Skip for spider/bot requests to avoid unnecessary API calls
|
||||
if (function_exists('homeproz_is_spider') && homeproz_is_spider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if no photos or no listing_id (can't fetch from API without it)
|
||||
if (empty($property->photos_count) || $property->photos_count <= 0) {
|
||||
return;
|
||||
}
|
||||
if (empty($property->listing_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media_table = $this->db->media_table();
|
||||
$listing_key = $property->listing_key;
|
||||
|
||||
// Single query to get media count and earliest expiry
|
||||
$media_stats = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT COUNT(*) as media_count, MIN(url_expires_at) as earliest_expiry
|
||||
FROM {$media_table}
|
||||
WHERE listing_key = %s",
|
||||
$listing_key
|
||||
));
|
||||
|
||||
$media_count = (int) ($media_stats ? $media_stats->media_count : 0);
|
||||
$earliest_expiry = $media_stats ? $media_stats->earliest_expiry : null;
|
||||
|
||||
// Check 1: Do we have fewer media records than photos?
|
||||
$needs_refresh = $media_count < $property->photos_count;
|
||||
|
||||
// Check 2: Are any URLs expired? (only check if we have records)
|
||||
if (!$needs_refresh && $media_count > 0 && $earliest_expiry) {
|
||||
$needs_refresh = strtotime($earliest_expiry) < time();
|
||||
}
|
||||
|
||||
if (!$needs_refresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh media from API
|
||||
$this->refresh_property_media($property->listing_key, $property->listing_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh media records for a property from the MLS API
|
||||
*
|
||||
* @param string $listing_key Property listing key
|
||||
* @param string $listing_id Property listing ID (MLS ID)
|
||||
*/
|
||||
private function refresh_property_media($listing_key, $listing_id) {
|
||||
$plugin = mls_plugin();
|
||||
$api_client = $plugin->get_api_client();
|
||||
$media_handler = $plugin->get_media_handler();
|
||||
|
||||
if (!$api_client || !$media_handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch property with media from API
|
||||
$property_data = $api_client->get_property_media($listing_id);
|
||||
|
||||
if (is_wp_error($property_data) || empty($property_data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync media records (URLs only, no downloads)
|
||||
if (isset($property_data['Media']) && is_array($property_data['Media'])) {
|
||||
$media_handler->sync_property_media($listing_key, $property_data['Media']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manual property by listing key or listing ID
|
||||
*
|
||||
@@ -979,8 +1131,7 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
if (!empty($args['property_type'])) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
$this->build_property_type_clause($args['property_type'], $where, $values);
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
@@ -1095,8 +1246,7 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
if (!empty($args['property_type'])) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
$this->build_property_type_clause($args['property_type'], $where, $values);
|
||||
}
|
||||
|
||||
if (!empty($args['city'])) {
|
||||
@@ -1327,8 +1477,7 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
if (!empty($args['property_type'])) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
$this->build_property_type_clause($args['property_type'], $where, $values);
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
|
||||
Reference in New Issue
Block a user