Manual property enhancements: MLS status sync, agent clone, description formatting
- Manual properties linked to MLS now inherit status (Active/Pending/Closed) and days_on_market from the MLS listing dynamically - Properties not in MLS default to Closed status - Clone feature now auto-populates listing agent by matching MLS ID to Agent CPT - Description formatter detects embedded headers (unpunctuated text after sentences) and splits them into separate paragraphs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,24 +191,62 @@ class MLS_CLI {
|
||||
private function show_rate_limits() {
|
||||
$rate_limiter = $this->plugin->get_rate_limiter();
|
||||
$status = $rate_limiter->get_status();
|
||||
$summary = $rate_limiter->get_usage_summary();
|
||||
|
||||
WP_CLI::line('=== Rate Limits ===');
|
||||
WP_CLI::line('=== MLS Grid Rate Limits ===');
|
||||
WP_CLI::line('');
|
||||
|
||||
// Requests
|
||||
WP_CLI::line('Requests:');
|
||||
WP_CLI::line(sprintf(
|
||||
'Hourly: %d / %d requests (%d remaining)',
|
||||
$status['hourly']['used'],
|
||||
$status['hourly']['limit'],
|
||||
$status['hourly']['remaining']
|
||||
' Hourly: %s / %s (%s%%)',
|
||||
number_format($status['hourly']['used']),
|
||||
number_format($status['hourly']['limit']),
|
||||
$summary['requests_hourly_pct']
|
||||
));
|
||||
WP_CLI::line(sprintf(
|
||||
'Daily: %d / %d requests (%d remaining)',
|
||||
$status['daily']['used'],
|
||||
$status['daily']['limit'],
|
||||
$status['daily']['remaining']
|
||||
' Daily: %s / %s (%s%%)',
|
||||
number_format($status['daily']['used']),
|
||||
number_format($status['daily']['limit']),
|
||||
$summary['requests_daily_pct']
|
||||
));
|
||||
WP_CLI::line('');
|
||||
|
||||
// Data transfer
|
||||
WP_CLI::line('Data Transfer:');
|
||||
WP_CLI::line(sprintf(
|
||||
' Hourly: %s / %s (%s%%)',
|
||||
size_format($status['data_hourly']['used']),
|
||||
size_format($status['data_hourly']['limit']),
|
||||
$summary['data_hourly_pct']
|
||||
));
|
||||
WP_CLI::line(sprintf(
|
||||
'Data: %s / 4GB this hour',
|
||||
size_format($status['bytes_this_hour'])
|
||||
' Daily: %s / %s (%s%%)',
|
||||
size_format($status['data_daily']['used']),
|
||||
size_format($status['data_daily']['limit']),
|
||||
$summary['data_daily_pct']
|
||||
));
|
||||
WP_CLI::line(sprintf(
|
||||
' Remaining today: %s GB',
|
||||
$summary['data_daily_remaining_gb']
|
||||
));
|
||||
WP_CLI::line('');
|
||||
|
||||
// Warnings
|
||||
if ($rate_limiter->is_approaching_limit(0.7)) {
|
||||
WP_CLI::warning('Approaching rate limits (>70% used)');
|
||||
}
|
||||
if ($rate_limiter->is_approaching_limit(0.9)) {
|
||||
WP_CLI::error('Critical: Near rate limit threshold (>90% used)', false);
|
||||
}
|
||||
|
||||
// Sync pacing info
|
||||
WP_CLI::line('Sync Pacing:');
|
||||
WP_CLI::line(sprintf(
|
||||
' Min interval: %s seconds between API requests',
|
||||
number_format(MLS_Rate_Limiter::SYNC_MIN_INTERVAL_MS / 1000, 2)
|
||||
));
|
||||
WP_CLI::line(' (Ensures max 50%% of daily quota used even if sync runs 24h)');
|
||||
WP_CLI::line('');
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,8 @@ class MLS_Cluster {
|
||||
|
||||
// Build WHERE clause
|
||||
// Exclude properties with invalid coordinates from map display
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
// Also exclude properties with no price or price < 100 (invalid data)
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0', 'list_price >= 100');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN, IA only)
|
||||
@@ -484,6 +485,11 @@ class MLS_Cluster {
|
||||
$street = implode(' ', $address_parts);
|
||||
$full_address = $street ? $street . ', ' . $property->city : $property->city;
|
||||
|
||||
// Get image URL with signature
|
||||
$image_url = function_exists('mls_get_image_url')
|
||||
? mls_get_image_url($property->listing_key, 1, 'thumb')
|
||||
: '';
|
||||
|
||||
$markers[] = array(
|
||||
'id' => $property->listing_key,
|
||||
'lat' => (float) $property->latitude,
|
||||
@@ -495,6 +501,7 @@ class MLS_Cluster {
|
||||
'baths' => $property->bathrooms_total,
|
||||
'sqft' => $property->living_area,
|
||||
'status' => $property->standard_status,
|
||||
'image' => $image_url,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -572,8 +579,8 @@ class MLS_Cluster {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
// Exclude properties with invalid coordinates
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
// Exclude properties with invalid coordinates or invalid price
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0', 'list_price >= 100');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN, IA only)
|
||||
|
||||
+1256
File diff suppressed because it is too large
Load Diff
@@ -464,6 +464,9 @@ class MLS_Media_Handler {
|
||||
* Uses MySQL advisory lock to ensure only one request downloads
|
||||
* a specific image at a time (prevents stampede on cold cache).
|
||||
*
|
||||
* Respects daily data budget - if approaching limit, will skip fetch
|
||||
* and return null (graceful degradation).
|
||||
*
|
||||
* @param object $media Media record
|
||||
* @return string|null Local URL on success, null on failure
|
||||
*/
|
||||
@@ -474,6 +477,17 @@ class MLS_Media_Handler {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check daily data budget before fetching
|
||||
$rate_limiter = mls_plugin()->get_rate_limiter();
|
||||
if (!$rate_limiter->can_fetch_image()) {
|
||||
$this->logger->warning('Daily data budget exhausted, skipping image fetch', array(
|
||||
'listing_key' => $media->listing_key,
|
||||
'media_key' => $media->media_key,
|
||||
'remaining_bytes' => $rate_limiter->get_daily_data_remaining(),
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
// Advisory lock key - unique per media record
|
||||
$lock_name = 'mls_media_' . $media->id;
|
||||
$lock_timeout = 35; // Slightly longer than HTTP timeout
|
||||
@@ -516,9 +530,16 @@ class MLS_Media_Handler {
|
||||
}
|
||||
|
||||
// Download the image
|
||||
$response = wp_remote_get($media->media_url, array(
|
||||
$request_args = array(
|
||||
'timeout' => 30,
|
||||
));
|
||||
);
|
||||
|
||||
// Allow skipping SSL verification if configured (for expired certs)
|
||||
if (defined('MLS_SKIP_SSL_VERIFY') && MLS_SKIP_SSL_VERIFY) {
|
||||
$request_args['sslverify'] = false;
|
||||
}
|
||||
|
||||
$response = wp_remote_get($media->media_url, $request_args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$this->logger->warning('Media fetch failed', array(
|
||||
@@ -545,6 +566,10 @@ class MLS_Media_Handler {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Record bytes downloaded against daily data cap
|
||||
$bytes_downloaded = strlen($body);
|
||||
$rate_limiter->record_data_transfer($bytes_downloaded);
|
||||
|
||||
// Determine extension
|
||||
$content_type = wp_remote_retrieve_header($response, 'content-type');
|
||||
$extension = $this->get_extension_from_content_type($content_type, $media->media_url);
|
||||
@@ -598,7 +623,7 @@ class MLS_Media_Handler {
|
||||
$this->logger->debug('Media fetched and cached', array(
|
||||
'listing_key' => $media->listing_key,
|
||||
'media_key' => $media->media_key,
|
||||
'original_size' => strlen($body),
|
||||
'bytes_downloaded' => $bytes_downloaded,
|
||||
'final_size' => $final_size,
|
||||
'converted' => $conversion['converted'],
|
||||
));
|
||||
|
||||
@@ -164,6 +164,9 @@ class MLS_Query {
|
||||
/**
|
||||
* Get properties matching criteria
|
||||
*
|
||||
* Queries both MLS and manual properties, excluding MLS entries
|
||||
* that have manual overrides.
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Property objects
|
||||
*/
|
||||
@@ -190,6 +193,7 @@ class MLS_Query {
|
||||
'year_built_max' => null,
|
||||
'listing_key' => null,
|
||||
'listing_id' => null,
|
||||
'agent_mls_id' => null, // Filter by list_agent_mls_id
|
||||
'search' => null, // Search in address/remarks
|
||||
'bounds' => null, // Map bounds: array(sw_lat, sw_lng, ne_lat, ne_lng)
|
||||
'center' => null, // Map center for distance sort: array(lat, lng)
|
||||
@@ -199,11 +203,15 @@ class MLS_Query {
|
||||
'orderby' => 'modification_timestamp',
|
||||
'order' => 'DESC',
|
||||
'include_media' => false,
|
||||
'include_manual' => true, // Include manual properties
|
||||
'fields' => '*', // Specific fields or *
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// Get manual override IDs to exclude from MLS query
|
||||
$override_ids = $args['include_manual'] ? $this->get_manual_override_listing_ids() : array();
|
||||
|
||||
// Build query
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
@@ -218,7 +226,8 @@ class MLS_Query {
|
||||
$sql = "SELECT {$select} FROM {$table}";
|
||||
|
||||
// WHERE conditions
|
||||
$where = array('mlg_can_view = 1');
|
||||
// Exclude properties with no price or price < 100 (invalid data)
|
||||
$where = array('mlg_can_view = 1', 'list_price >= 100');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN and IA only)
|
||||
@@ -230,6 +239,12 @@ class MLS_Query {
|
||||
// Exclude TBD addresses
|
||||
$where[] = $this->get_tbd_exclusion_filter();
|
||||
|
||||
// Exclude MLS properties that have manual overrides
|
||||
if (!empty($override_ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($override_ids), '%s'));
|
||||
$where[] = $wpdb->prepare("listing_id NOT IN ({$placeholders})", $override_ids);
|
||||
}
|
||||
|
||||
if ($args['status']) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
@@ -270,6 +285,11 @@ class MLS_Query {
|
||||
$values[] = $args['county'];
|
||||
}
|
||||
|
||||
if ($args['agent_mls_id']) {
|
||||
$where[] = 'list_agent_mls_id = %s';
|
||||
$values[] = $args['agent_mls_id'];
|
||||
}
|
||||
|
||||
if ($args['min_price']) {
|
||||
$where[] = 'list_price >= %d';
|
||||
$values[] = (int) $args['min_price'];
|
||||
@@ -397,9 +417,59 @@ class MLS_Query {
|
||||
$values[] = (int) $args['limit'];
|
||||
$values[] = (int) $args['offset'];
|
||||
|
||||
// Execute
|
||||
// Execute MLS query
|
||||
$results = $wpdb->get_results($wpdb->prepare($sql, $values));
|
||||
|
||||
// Query manual properties with same filters
|
||||
if ($args['include_manual']) {
|
||||
$manual_results = $this->get_manual_properties($args);
|
||||
if (!empty($manual_results)) {
|
||||
// Merge manual properties with MLS results
|
||||
$results = array_merge($manual_results, $results);
|
||||
|
||||
// Re-sort: HomeProz first, then featured, then by orderby
|
||||
$featured_ids = !empty($args['featured_ids']) ? (array) $args['featured_ids'] : array();
|
||||
$order_desc = strtoupper($args['order']) !== 'ASC';
|
||||
$orderby = $args['orderby'];
|
||||
|
||||
usort($results, function($a, $b) use ($featured_ids, $order_desc, $orderby) {
|
||||
// 1. HomeProz listings first
|
||||
$a_homeproz = !empty($a->is_homeproz) ? 1 : 0;
|
||||
$b_homeproz = !empty($b->is_homeproz) ? 1 : 0;
|
||||
if ($a_homeproz !== $b_homeproz) {
|
||||
return $b_homeproz - $a_homeproz;
|
||||
}
|
||||
|
||||
// 2. Featured listings second
|
||||
$a_featured = in_array($a->listing_id, $featured_ids) || !empty($a->is_featured) ? 1 : 0;
|
||||
$b_featured = in_array($b->listing_id, $featured_ids) || !empty($b->is_featured) ? 1 : 0;
|
||||
if ($a_featured !== $b_featured) {
|
||||
return $b_featured - $a_featured;
|
||||
}
|
||||
|
||||
// 3. Order by specified field
|
||||
$a_val = isset($a->$orderby) ? $a->$orderby : '';
|
||||
$b_val = isset($b->$orderby) ? $b->$orderby : '';
|
||||
|
||||
if ($a_val === $b_val) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Compare as numbers if numeric
|
||||
if (is_numeric($a_val) && is_numeric($b_val)) {
|
||||
$cmp = ($a_val < $b_val) ? -1 : 1;
|
||||
} else {
|
||||
$cmp = strcmp($a_val, $b_val);
|
||||
}
|
||||
|
||||
return $order_desc ? -$cmp : $cmp;
|
||||
});
|
||||
|
||||
// Apply offset and limit after merge
|
||||
$results = array_slice($results, (int) $args['offset'], (int) $args['limit']);
|
||||
}
|
||||
}
|
||||
|
||||
// Include media if requested
|
||||
if ($args['include_media'] && $results) {
|
||||
foreach ($results as &$property) {
|
||||
@@ -410,15 +480,169 @@ class MLS_Query {
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual properties matching criteria
|
||||
*
|
||||
* @param array $args Query arguments (same as get_properties)
|
||||
* @return array Manual property objects
|
||||
*/
|
||||
private function get_manual_properties($args) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->manual_properties_table();
|
||||
|
||||
// Check if table exists
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||
DB_NAME,
|
||||
$table
|
||||
));
|
||||
|
||||
if (!$table_exists) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM {$table}";
|
||||
$where = array("standard_status != 'Withdrawn'");
|
||||
$values = array();
|
||||
|
||||
if ($args['status']) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
}
|
||||
|
||||
if ($args['property_type']) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
}
|
||||
|
||||
if ($args['city']) {
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
}
|
||||
|
||||
if ($args['county']) {
|
||||
$where[] = 'county = %s';
|
||||
$values[] = $args['county'];
|
||||
}
|
||||
|
||||
if ($args['postal_code']) {
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
}
|
||||
|
||||
if ($args['min_price']) {
|
||||
$where[] = 'list_price >= %d';
|
||||
$values[] = (int) $args['min_price'];
|
||||
}
|
||||
|
||||
if ($args['max_price']) {
|
||||
$where[] = 'list_price <= %d';
|
||||
$values[] = (int) $args['max_price'];
|
||||
}
|
||||
|
||||
if ($args['min_beds']) {
|
||||
$where[] = 'bedrooms_total >= %d';
|
||||
$values[] = (int) $args['min_beds'];
|
||||
}
|
||||
|
||||
if ($args['max_beds']) {
|
||||
$where[] = 'bedrooms_total <= %d';
|
||||
$values[] = (int) $args['max_beds'];
|
||||
}
|
||||
|
||||
if ($args['min_baths']) {
|
||||
$where[] = 'bathrooms_total >= %d';
|
||||
$values[] = (int) $args['min_baths'];
|
||||
}
|
||||
|
||||
if ($args['listing_key']) {
|
||||
$where[] = 'listing_key = %s';
|
||||
$values[] = $args['listing_key'];
|
||||
}
|
||||
|
||||
if ($args['listing_id']) {
|
||||
$where[] = 'listing_id = %s';
|
||||
$values[] = $args['listing_id'];
|
||||
}
|
||||
|
||||
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)';
|
||||
$values[] = $search_term;
|
||||
$values[] = $search_term;
|
||||
$values[] = $search_term;
|
||||
$values[] = $search_term;
|
||||
}
|
||||
|
||||
// Map bounds filtering
|
||||
if ($args['bounds'] && is_array($args['bounds']) && count($args['bounds']) === 4) {
|
||||
list($sw_lat, $sw_lng, $ne_lat, $ne_lng) = $args['bounds'];
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$where[] = 'latitude IS NOT NULL';
|
||||
$where[] = 'longitude IS NOT NULL';
|
||||
$values[] = (float) $sw_lat;
|
||||
$values[] = (float) $ne_lat;
|
||||
$values[] = (float) $sw_lng;
|
||||
$values[] = (float) $ne_lng;
|
||||
}
|
||||
|
||||
// Radius search for manual properties
|
||||
if ($args['center_lat'] && $args['center_lng']) {
|
||||
$lat = (float) $args['center_lat'];
|
||||
$lng = (float) $args['center_lng'];
|
||||
$radius = (int) $args['radius'];
|
||||
|
||||
// Use simple bounding box for manual properties (no spatial index)
|
||||
$lat_delta = $radius / 69.0;
|
||||
$lng_delta = $radius / (69.0 * cos(deg2rad($lat)));
|
||||
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$values[] = $lat - $lat_delta;
|
||||
$values[] = $lat + $lat_delta;
|
||||
$values[] = $lng - $lng_delta;
|
||||
$values[] = $lng + $lng_delta;
|
||||
}
|
||||
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
|
||||
// Execute
|
||||
if (!empty($values)) {
|
||||
$results = $wpdb->get_results($wpdb->prepare($sql, $values));
|
||||
} else {
|
||||
$results = $wpdb->get_results($sql);
|
||||
}
|
||||
|
||||
// Normalize results to match MLS schema
|
||||
foreach ($results as $key => $property) {
|
||||
$results[$key] = $this->normalize_manual_property($property);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single property
|
||||
*
|
||||
* Checks manual properties first (for overrides), then MLS.
|
||||
*
|
||||
* @param string $identifier Listing key or listing ID
|
||||
* @param bool $skip_manual_override Skip manual property override (for A/B testing)
|
||||
* @return object|null Property object
|
||||
*/
|
||||
public function get_property($identifier) {
|
||||
public function get_property($identifier, $skip_manual_override = false) {
|
||||
global $wpdb;
|
||||
|
||||
// Check manual properties first (they can override MLS)
|
||||
if (!$skip_manual_override) {
|
||||
$manual = $this->get_manual_property($identifier);
|
||||
if ($manual) {
|
||||
return $manual;
|
||||
}
|
||||
}
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
// Try listing_key first
|
||||
@@ -435,18 +659,193 @@ class MLS_Query {
|
||||
));
|
||||
}
|
||||
|
||||
// If found by listing_id, check if there's a manual override
|
||||
if (!$skip_manual_override && $property && $property->listing_id) {
|
||||
$override_ids = $this->get_manual_override_listing_ids();
|
||||
if (in_array($property->listing_id, $override_ids)) {
|
||||
// Manual override exists, return the manual version
|
||||
return $this->get_manual_property($property->listing_id);
|
||||
}
|
||||
}
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manual property by listing key or listing ID
|
||||
*
|
||||
* @param string $identifier Listing key (MANUAL-xxx) or listing_id
|
||||
* @return object|null Property object normalized to match MLS schema
|
||||
*/
|
||||
private function get_manual_property($identifier) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->manual_properties_table();
|
||||
|
||||
// Check if table exists
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||
DB_NAME,
|
||||
$table
|
||||
));
|
||||
|
||||
if (!$table_exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try by listing_key first (MANUAL-xxx format)
|
||||
$property = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE listing_key = %s AND standard_status != 'Withdrawn'",
|
||||
$identifier
|
||||
));
|
||||
|
||||
if (!$property) {
|
||||
// Try by listing_id (MLS ID for overrides)
|
||||
$property = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE listing_id = %s AND standard_status != 'Withdrawn'",
|
||||
$identifier
|
||||
));
|
||||
}
|
||||
|
||||
if ($property) {
|
||||
// Normalize to match MLS schema
|
||||
$property = $this->normalize_manual_property($property);
|
||||
}
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a manual property to match MLS schema
|
||||
*
|
||||
* @param object $property Manual property row
|
||||
* @return object Normalized property
|
||||
*/
|
||||
private function normalize_manual_property($property) {
|
||||
global $wpdb;
|
||||
|
||||
// Set defaults for MLS-specific fields
|
||||
$property->originating_system = 'manual';
|
||||
$property->mls_status = null;
|
||||
$property->mlg_can_view = 1;
|
||||
$property->original_list_price = $property->original_list_price ?? $property->list_price;
|
||||
|
||||
// For manual properties linked to MLS, use the MLS status and days_on_market
|
||||
// If not in MLS (no listing_id or MLS listing not found), default to Closed
|
||||
$property->days_on_market = null; // Default to null (won't display)
|
||||
|
||||
if (!empty($property->listing_id)) {
|
||||
$mls_table = $this->db->properties_table();
|
||||
$mls_data = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT standard_status, days_on_market FROM {$mls_table} WHERE listing_id = %s AND mlg_can_view = 1",
|
||||
$property->listing_id
|
||||
));
|
||||
|
||||
if ($mls_data) {
|
||||
$property->standard_status = $mls_data->standard_status;
|
||||
$property->mls_status = $mls_data->standard_status;
|
||||
$property->days_on_market = $mls_data->days_on_market;
|
||||
} else {
|
||||
// MLS listing no longer exists - assume Closed
|
||||
$property->standard_status = 'Closed';
|
||||
}
|
||||
} else {
|
||||
// No MLS link - pure manual property, assume Closed
|
||||
$property->standard_status = 'Closed';
|
||||
}
|
||||
|
||||
// Get agent info from linked Agent CPT
|
||||
if (!empty($property->list_agent_post_id)) {
|
||||
$agent_name = get_the_title($property->list_agent_post_id);
|
||||
$agent_mls_id = get_field('mls_id', $property->list_agent_post_id);
|
||||
$property->list_agent_name = $agent_name;
|
||||
$property->list_agent_mls_id = $agent_mls_id;
|
||||
$property->list_agent_key = null;
|
||||
} else {
|
||||
$property->list_agent_name = null;
|
||||
$property->list_agent_mls_id = null;
|
||||
$property->list_agent_key = null;
|
||||
}
|
||||
|
||||
// Office info (manual listings are HomeProz)
|
||||
$property->list_office_key = null;
|
||||
$property->list_office_mls_id = $property->is_homeproz ? (defined('MLS_HOMEPROZ_OFFICE_ID') ? MLS_HOMEPROZ_OFFICE_ID : null) : null;
|
||||
$property->list_office_name = $property->is_homeproz ? 'HomeProz Real Estate' : null;
|
||||
|
||||
// Date fields
|
||||
$property->modification_timestamp = $property->updated_at;
|
||||
$property->photos_change_timestamp = $property->updated_at;
|
||||
$property->listing_contract_date = $property->list_date;
|
||||
// days_on_market is set above from MLS data (or null if not in MLS)
|
||||
$property->media_expires_at = null;
|
||||
$property->coordinates_invalid = 0;
|
||||
|
||||
// Location column for spatial queries (null for manual)
|
||||
$property->location = null;
|
||||
|
||||
// Raw data
|
||||
$property->raw_data = null;
|
||||
|
||||
// Mark as manual for frontend display logic
|
||||
$property->is_manual = 1;
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get listing IDs that have manual overrides
|
||||
*
|
||||
* @return array Array of MLS listing_id values
|
||||
*/
|
||||
private function get_manual_override_listing_ids() {
|
||||
global $wpdb;
|
||||
|
||||
// Cache result for this request
|
||||
static $override_ids = null;
|
||||
if ($override_ids !== null) {
|
||||
return $override_ids;
|
||||
}
|
||||
|
||||
$table = $this->db->manual_properties_table();
|
||||
|
||||
// Check if table exists
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||
DB_NAME,
|
||||
$table
|
||||
));
|
||||
|
||||
if (!$table_exists) {
|
||||
$override_ids = array();
|
||||
return $override_ids;
|
||||
}
|
||||
|
||||
$override_ids = $wpdb->get_col(
|
||||
"SELECT listing_id FROM {$table}
|
||||
WHERE listing_id IS NOT NULL
|
||||
AND listing_id != ''
|
||||
AND standard_status != 'Withdrawn'"
|
||||
);
|
||||
|
||||
return array_filter($override_ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get media for a property
|
||||
*
|
||||
* Handles both MLS and manual properties.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @return array Media objects
|
||||
*/
|
||||
public function get_property_media($listing_key) {
|
||||
global $wpdb;
|
||||
|
||||
// Check if this is a manual property
|
||||
if (strpos($listing_key, 'MANUAL-') === 0) {
|
||||
return MLS_Manual_Property::get_manual_property_images($listing_key);
|
||||
}
|
||||
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->db->media_table()}
|
||||
WHERE listing_key = %s
|
||||
@@ -540,15 +939,23 @@ class MLS_Query {
|
||||
/**
|
||||
* Get property count
|
||||
*
|
||||
* Counts both MLS and manual properties.
|
||||
*
|
||||
* @param array $args Filter arguments (same as get_properties)
|
||||
* @return int Count
|
||||
*/
|
||||
public function get_count($args = array()) {
|
||||
global $wpdb;
|
||||
|
||||
$include_manual = !isset($args['include_manual']) || $args['include_manual'];
|
||||
|
||||
// Get override IDs to exclude from MLS count
|
||||
$override_ids = $include_manual ? $this->get_manual_override_listing_ids() : array();
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
$where = array('mlg_can_view = 1');
|
||||
// Exclude properties with no price or price < 100 (invalid data)
|
||||
$where = array('mlg_can_view = 1', 'list_price >= 100');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN and IA only)
|
||||
@@ -560,6 +967,12 @@ class MLS_Query {
|
||||
// Exclude TBD addresses
|
||||
$where[] = $this->get_tbd_exclusion_filter();
|
||||
|
||||
// Exclude MLS properties that have manual overrides
|
||||
if (!empty($override_ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($override_ids), '%s'));
|
||||
$where[] = $wpdb->prepare("listing_id NOT IN ({$placeholders})", $override_ids);
|
||||
}
|
||||
|
||||
if (!empty($args['status'])) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
@@ -636,6 +1049,116 @@ class MLS_Query {
|
||||
|
||||
$sql = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $where);
|
||||
|
||||
if (!empty($values)) {
|
||||
$mls_count = (int) $wpdb->get_var($wpdb->prepare($sql, $values));
|
||||
} else {
|
||||
$mls_count = (int) $wpdb->get_var($sql);
|
||||
}
|
||||
|
||||
// Add manual property count
|
||||
if ($include_manual) {
|
||||
$manual_count = $this->get_manual_count($args);
|
||||
return $mls_count + $manual_count;
|
||||
}
|
||||
|
||||
return $mls_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual property count
|
||||
*
|
||||
* @param array $args Filter arguments
|
||||
* @return int Count
|
||||
*/
|
||||
private function get_manual_count($args) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->manual_properties_table();
|
||||
|
||||
// Check if table exists
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||
DB_NAME,
|
||||
$table
|
||||
));
|
||||
|
||||
if (!$table_exists) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$where = array("standard_status != 'Withdrawn'");
|
||||
$values = array();
|
||||
|
||||
if (!empty($args['status'])) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
}
|
||||
|
||||
if (!empty($args['property_type'])) {
|
||||
$where[] = 'property_type = %s';
|
||||
$values[] = $args['property_type'];
|
||||
}
|
||||
|
||||
if (!empty($args['city'])) {
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
} elseif (!empty($args['postal_code'])) {
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
} elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
|
||||
$lat = (float) $args['center_lat'];
|
||||
$lng = (float) $args['center_lng'];
|
||||
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
|
||||
$lat_delta = $radius / 69.0;
|
||||
$lng_delta = $radius / (69.0 * cos(deg2rad($lat)));
|
||||
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$values[] = $lat - $lat_delta;
|
||||
$values[] = $lat + $lat_delta;
|
||||
$values[] = $lng - $lng_delta;
|
||||
$values[] = $lng + $lng_delta;
|
||||
}
|
||||
|
||||
if (!empty($args['county'])) {
|
||||
$where[] = 'county = %s';
|
||||
$values[] = $args['county'];
|
||||
}
|
||||
|
||||
if (!empty($args['min_price'])) {
|
||||
$where[] = 'list_price >= %d';
|
||||
$values[] = (int) $args['min_price'];
|
||||
}
|
||||
|
||||
if (!empty($args['max_price'])) {
|
||||
$where[] = 'list_price <= %d';
|
||||
$values[] = (int) $args['max_price'];
|
||||
}
|
||||
|
||||
if (!empty($args['min_beds'])) {
|
||||
$where[] = 'bedrooms_total >= %d';
|
||||
$values[] = (int) $args['min_beds'];
|
||||
}
|
||||
|
||||
if (!empty($args['min_baths'])) {
|
||||
$where[] = 'bathrooms_total >= %d';
|
||||
$values[] = (int) $args['min_baths'];
|
||||
}
|
||||
|
||||
if (!empty($args['bounds']) && is_array($args['bounds']) && count($args['bounds']) === 4) {
|
||||
list($sw_lat, $sw_lng, $ne_lat, $ne_lng) = $args['bounds'];
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$where[] = 'latitude IS NOT NULL';
|
||||
$where[] = 'longitude IS NOT NULL';
|
||||
$values[] = (float) $sw_lat;
|
||||
$values[] = (float) $ne_lat;
|
||||
$values[] = (float) $sw_lng;
|
||||
$values[] = (float) $ne_lng;
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $where);
|
||||
|
||||
if (!empty($values)) {
|
||||
return (int) $wpdb->get_var($wpdb->prepare($sql, $values));
|
||||
}
|
||||
@@ -785,8 +1308,8 @@ class MLS_Query {
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
// Exclude properties with invalid coordinates from map bounds
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
// Exclude properties with invalid coordinates or invalid price from map bounds
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0', 'list_price >= 100');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN and IA only)
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
/**
|
||||
* Rate limiter class for MLS Grid API compliance
|
||||
*
|
||||
* MLS Grid Rate Limits:
|
||||
* - 2 requests per second
|
||||
* - 7,200 requests per hour
|
||||
* - 40,000 requests per day
|
||||
* - 4GB data per hour
|
||||
* MLS Grid Rate Limits (warning thresholds):
|
||||
* - 4 requests per second (suspension at 6)
|
||||
* - 7,200 requests per hour (suspension at 18,000)
|
||||
* - 40,000 requests per 24 hours (suspension at 60,000)
|
||||
* - 3GB data per hour / 40GB per 24 hours (suspension at 4GB/60GB)
|
||||
*
|
||||
* Our strategy: Throttle sync operations to use max 50% of daily quota
|
||||
* even if running continuously for 24 hours. This leaves 50% budget
|
||||
* for on-demand image fetches and other operations.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
@@ -16,17 +20,36 @@ if (!defined('ABSPATH')) {
|
||||
class MLS_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Rate limit constants
|
||||
* MLS Grid absolute limits (for reference)
|
||||
*/
|
||||
const MLSGRID_LIMIT_PER_SECOND = 4;
|
||||
const MLSGRID_LIMIT_PER_HOUR = 7200;
|
||||
const MLSGRID_LIMIT_PER_DAY = 40000;
|
||||
const MLSGRID_BYTES_PER_HOUR = 3221225472; // 3GB
|
||||
const MLSGRID_BYTES_PER_DAY = 42949672960; // 40GB
|
||||
|
||||
/**
|
||||
* Sync operation limits (50% of daily quota paced over 24 hours)
|
||||
*
|
||||
* Goal: If sync ran continuously for 24h, use max 50% of daily quota
|
||||
* - 20,000 requests / 86,400 seconds = 0.23 RPS (~4.3s between requests)
|
||||
* - 20GB data / 86,400 seconds = ~243KB/s average
|
||||
*/
|
||||
const SYNC_REQUESTS_PER_DAY = 20000; // 50% of 40,000
|
||||
const SYNC_BYTES_PER_DAY = 21474836480; // 20GB (50% of 40GB)
|
||||
const SYNC_MIN_INTERVAL_MS = 4320; // 86400000ms / 20000 = 4.32s between requests
|
||||
|
||||
/**
|
||||
* Rate limit constants (used for tracking against MLS Grid limits)
|
||||
*/
|
||||
const LIMIT_PER_SECOND = 2;
|
||||
const LIMIT_PER_HOUR = 7200;
|
||||
const LIMIT_PER_DAY = 40000;
|
||||
const LIMIT_BYTES_PER_HOUR = 4294967296; // 4GB
|
||||
const LIMIT_BYTES_PER_HOUR = 3221225472; // 3GB
|
||||
const LIMIT_BYTES_PER_DAY = 42949672960; // 40GB
|
||||
|
||||
/**
|
||||
* Window types
|
||||
*/
|
||||
const WINDOW_SECOND = 'second';
|
||||
const WINDOW_HOUR = 'hour';
|
||||
const WINDOW_DAY = 'day';
|
||||
|
||||
@@ -52,14 +75,18 @@ class MLS_Rate_Limiter {
|
||||
/**
|
||||
* Check if we can make a request (and wait if needed)
|
||||
*
|
||||
* For sync operations, this enforces the 50% daily quota pacing.
|
||||
* The minimum interval between requests ensures that even continuous
|
||||
* syncing won't exceed 50% of the daily quota.
|
||||
*
|
||||
* @param bool $wait Whether to wait if rate limited
|
||||
* @return bool True if request can proceed
|
||||
*/
|
||||
public function check_and_wait($wait = true) {
|
||||
// Check per-second limit (most restrictive)
|
||||
$this->enforce_per_second_limit();
|
||||
// Enforce sync pacing (4.32s between requests for 50% daily quota)
|
||||
$this->enforce_sync_pacing();
|
||||
|
||||
// Check hourly limit
|
||||
// Check hourly limit (hard stop if approaching MLS Grid limits)
|
||||
if (!$this->check_limit(self::WINDOW_HOUR, self::LIMIT_PER_HOUR)) {
|
||||
if ($wait) {
|
||||
$this->wait_for_window(self::WINDOW_HOUR);
|
||||
@@ -68,7 +95,7 @@ class MLS_Rate_Limiter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily limit
|
||||
// Check daily limit (hard stop if approaching MLS Grid limits)
|
||||
if (!$this->check_limit(self::WINDOW_DAY, self::LIMIT_PER_DAY)) {
|
||||
if ($wait) {
|
||||
$this->wait_for_window(self::WINDOW_DAY);
|
||||
@@ -81,11 +108,14 @@ class MLS_Rate_Limiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce per-second rate limit
|
||||
* Enforce sync operation pacing
|
||||
*
|
||||
* Ensures minimum interval between sync requests so that
|
||||
* 24 hours of continuous syncing uses max 50% of daily quota.
|
||||
*/
|
||||
private function enforce_per_second_limit() {
|
||||
private function enforce_sync_pacing() {
|
||||
$now = microtime(true);
|
||||
$min_interval = 1.0 / self::LIMIT_PER_SECOND; // 0.5 seconds
|
||||
$min_interval = self::SYNC_MIN_INTERVAL_MS / 1000.0; // Convert ms to seconds (4.32s)
|
||||
|
||||
if ($this->last_request_time > 0) {
|
||||
$elapsed = $now - $this->last_request_time;
|
||||
@@ -141,9 +171,6 @@ class MLS_Rate_Limiter {
|
||||
$now = current_time('timestamp');
|
||||
|
||||
switch ($window_type) {
|
||||
case self::WINDOW_SECOND:
|
||||
return gmdate('Y-m-d H:i:s', $now);
|
||||
|
||||
case self::WINDOW_HOUR:
|
||||
return gmdate('Y-m-d H:00:00', $now);
|
||||
|
||||
@@ -261,6 +288,9 @@ class MLS_Rate_Limiter {
|
||||
* @return array Rate limit status
|
||||
*/
|
||||
public function get_status() {
|
||||
$bytes_hour = $this->get_bytes_this_hour();
|
||||
$bytes_day = $this->get_bytes_today();
|
||||
|
||||
return array(
|
||||
'hourly' => array(
|
||||
'used' => $this->get_window_count(self::WINDOW_HOUR),
|
||||
@@ -272,7 +302,18 @@ class MLS_Rate_Limiter {
|
||||
'limit' => self::LIMIT_PER_DAY,
|
||||
'remaining' => max(0, self::LIMIT_PER_DAY - $this->get_window_count(self::WINDOW_DAY)),
|
||||
),
|
||||
'bytes_this_hour' => $this->get_bytes_this_hour(),
|
||||
'data_hourly' => array(
|
||||
'used' => $bytes_hour,
|
||||
'limit' => self::LIMIT_BYTES_PER_HOUR,
|
||||
'remaining' => max(0, self::LIMIT_BYTES_PER_HOUR - $bytes_hour),
|
||||
),
|
||||
'data_daily' => array(
|
||||
'used' => $bytes_day,
|
||||
'limit' => self::LIMIT_BYTES_PER_DAY,
|
||||
'remaining' => max(0, self::LIMIT_BYTES_PER_DAY - $bytes_day),
|
||||
),
|
||||
// Legacy fields for backward compatibility
|
||||
'bytes_this_hour' => $bytes_hour,
|
||||
'bytes_limit' => self::LIMIT_BYTES_PER_HOUR,
|
||||
);
|
||||
}
|
||||
@@ -282,7 +323,7 @@ class MLS_Rate_Limiter {
|
||||
*
|
||||
* @return int Bytes
|
||||
*/
|
||||
private function get_bytes_this_hour() {
|
||||
public function get_bytes_this_hour() {
|
||||
global $wpdb;
|
||||
|
||||
$window_start = $this->get_window_start(self::WINDOW_HOUR);
|
||||
@@ -297,6 +338,103 @@ class MLS_Rate_Limiter {
|
||||
return $bytes ? (int) $bytes : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bytes transferred today
|
||||
*
|
||||
* @return int Bytes
|
||||
*/
|
||||
public function get_bytes_today() {
|
||||
global $wpdb;
|
||||
|
||||
$window_start = $this->get_window_start(self::WINDOW_DAY);
|
||||
|
||||
$bytes = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT bytes_transferred FROM {$this->db->rate_limits_table()}
|
||||
WHERE window_type = %s AND window_start = %s",
|
||||
self::WINDOW_DAY,
|
||||
$window_start
|
||||
));
|
||||
|
||||
return $bytes ? (int) $bytes : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining daily data budget
|
||||
*
|
||||
* @return int Remaining bytes
|
||||
*/
|
||||
public function get_daily_data_remaining() {
|
||||
return max(0, self::LIMIT_BYTES_PER_DAY - $this->get_bytes_today());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can fetch an image based on remaining daily data budget
|
||||
*
|
||||
* @param int $estimated_bytes Estimated size of image (default 400KB)
|
||||
* @return bool True if we have budget for this image
|
||||
*/
|
||||
public function can_fetch_image($estimated_bytes = 409600) {
|
||||
return $this->get_daily_data_remaining() > $estimated_bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record data transfer (for image downloads, separate from API requests)
|
||||
*
|
||||
* This tracks bytes against the daily data cap without incrementing
|
||||
* the request count (since image fetches aren't API requests).
|
||||
*
|
||||
* @param int $bytes Bytes transferred
|
||||
*/
|
||||
public function record_data_transfer($bytes) {
|
||||
global $wpdb;
|
||||
|
||||
if ($bytes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record for hourly window (data only, no request count)
|
||||
$this->increment_data_only(self::WINDOW_HOUR, $bytes);
|
||||
|
||||
// Record for daily window (data only, no request count)
|
||||
$this->increment_data_only(self::WINDOW_DAY, $bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment data transfer for a window without incrementing request count
|
||||
*
|
||||
* @param string $window_type Window type
|
||||
* @param int $bytes Bytes transferred
|
||||
*/
|
||||
private function increment_data_only($window_type, $bytes) {
|
||||
global $wpdb;
|
||||
|
||||
$window_start = $this->get_window_start($window_type);
|
||||
|
||||
// Try to update existing record
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->db->rate_limits_table()}
|
||||
SET bytes_transferred = bytes_transferred + %d
|
||||
WHERE window_type = %s AND window_start = %s",
|
||||
$bytes,
|
||||
$window_type,
|
||||
$window_start
|
||||
));
|
||||
|
||||
// If no record existed, insert new one (request_count = 0 since this is data-only)
|
||||
if (0 === $updated) {
|
||||
$wpdb->insert(
|
||||
$this->db->rate_limits_table(),
|
||||
array(
|
||||
'window_type' => $window_type,
|
||||
'window_start' => $window_start,
|
||||
'request_count' => 0,
|
||||
'bytes_transferred' => $bytes,
|
||||
),
|
||||
array('%s', '%s', '%d', '%d')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're approaching rate limits
|
||||
*
|
||||
@@ -308,8 +446,26 @@ class MLS_Rate_Limiter {
|
||||
|
||||
$hourly_pct = $status['hourly']['used'] / $status['hourly']['limit'];
|
||||
$daily_pct = $status['daily']['used'] / $status['daily']['limit'];
|
||||
$data_daily_pct = $status['data_daily']['used'] / $status['data_daily']['limit'];
|
||||
|
||||
return $hourly_pct >= $threshold || $daily_pct >= $threshold;
|
||||
return $hourly_pct >= $threshold || $daily_pct >= $threshold || $data_daily_pct >= $threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of current usage for logging/display
|
||||
*
|
||||
* @return array Summary with percentages
|
||||
*/
|
||||
public function get_usage_summary() {
|
||||
$status = $this->get_status();
|
||||
|
||||
return array(
|
||||
'requests_hourly_pct' => round(($status['hourly']['used'] / $status['hourly']['limit']) * 100, 1),
|
||||
'requests_daily_pct' => round(($status['daily']['used'] / $status['daily']['limit']) * 100, 1),
|
||||
'data_hourly_pct' => round(($status['data_hourly']['used'] / $status['data_hourly']['limit']) * 100, 1),
|
||||
'data_daily_pct' => round(($status['data_daily']['used'] / $status['data_daily']['limit']) * 100, 1),
|
||||
'data_daily_remaining_gb' => round($status['data_daily']['remaining'] / 1073741824, 2),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user