get_type() === 'variation' ? (int) $product->get_parent_id() : (int) $product->get_id(); } function rayachat_mark_product($product_id) { // The save inside a stock update, and the one WooCommerce makes of a // variable product at the end of the request to sum up its variations // (after a sale too), are not the owner's edits. if (isset($GLOBALS['rayachat_stock_writing'][(int) $product_id]) || doing_action('shutdown')) { return; } $id = rayachat_product_of($product_id); if ($id) { $GLOBALS['rayachat_dirty'][$id] = true; } } function rayachat_mark_stock($product_id) { $id = rayachat_product_of($product_id); if ($id) { $GLOBALS['rayachat_stock_dirty'][$id] = true; } } // wc_update_product_stock() — orders, refunds, stock imports — writes the // stock, saves the product, then announces the stock. Only that path fires this. function rayachat_on_stock_written($product_id) { $GLOBALS['rayachat_stock_writing'][(int) $product_id] = true; } function rayachat_on_stock($product) { $id = is_object($product) ? $product->get_id() : (int) $product; unset($GLOBALS['rayachat_stock_writing'][$id]); rayachat_mark_stock(is_object($product) ? $product : $id); } function rayachat_on_delete($post_id) { if (in_array(get_post_type($post_id), array('product', 'product_variation'), true)) { rayachat_mark_product($post_id); } } // Minutes between a change and its push, as the owner chose on the plugin's // page. «Straight away» (0) went in 1.6.0: its push held the saving request, // a customer's checkout among them, until Rayachat answered. A site that had // it waits the shortest choice instead. function rayachat_sync_delay() { $settings = rayachat_settings(); return isset($settings['delay']) ? max(5, (int) $settings['delay']) : RAYACHAT_DEFAULT_DELAY; } /** * Runs at the end of every request, so it reads nothing unless something * changed. A product changed back to what it was arrives 'unchanged', so * nothing is written. */ function rayachat_flush_dirty() { $edited = array_map('intval', array_keys($GLOBALS['rayachat_dirty'])); $sold = array_values(array_diff(array_map('intval', array_keys($GLOBALS['rayachat_stock_dirty'])), $edited)); if ((!$edited && !$sold) || !rayachat_sync_active() || !function_exists('as_schedule_single_action')) { return; } $wait = rayachat_sync_delay() * MINUTE_IN_SECONDS; if ($edited) { rayachat_queue_changes($edited, $wait, true); } if ($sold) { rayachat_queue_changes($sold, $wait, false); } } /** * Queues each product to be sent after `$wait` seconds. `$restart`: the * owner's edit, which starts a running wait over (up to the cap); otherwise a * running wait is left as it is. */ function rayachat_queue_changes(array $ids, $wait, $restart) { if (count($ids) > RAYACHAT_SYNC_PAGE) { // A bulk edit or a stock import: one wait for the whole batch, without // looking at each product's own timer one query at a time. foreach (array_chunk($ids, RAYACHAT_SYNC_PAGE) as $chunk) { as_schedule_single_action(time() + $wait, 'rayachat_sync_products', array($chunk), RAYACHAT_SYNC_GROUP); } return; } foreach ($ids as $id) { $args = array(array($id)); if (!$restart) { // A pending wait reads the product after this sale. One already // running may have read it before, so it gets a successor. Asked // once, for pending only: this runs at the end of a checkout, and // as_next_scheduled_action() asks twice and loads the action. $pending = as_get_scheduled_actions(array( 'hook' => 'rayachat_sync_products', 'args' => $args, 'group' => RAYACHAT_SYNC_GROUP, 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => 1, 'orderby' => 'none', ), 'ids'); if (!$pending) { as_schedule_single_action(time() + $wait, 'rayachat_sync_products', $args, RAYACHAT_SYNC_GROUP); } continue; } $key = 'rayachat_wait_' . $id; // When this product's oldest unsent edit happened, kept with the wait // it belongs to. A note from any other wait (one a sale started, or // one already sent) starts a new count, so a sale never has to clear it. $cancelled = as_unschedule_action('rayachat_sync_products', $args, RAYACHAT_SYNC_GROUP); $note = $cancelled ? get_transient($key) : false; $first = isset($note['wait'], $note['first']) && (int) $note['wait'] === (int) $cancelled ? (int) $note['first'] : time(); $scheduled = as_schedule_single_action(min(time() + $wait, $first + 2 * $wait), 'rayachat_sync_products', $args, RAYACHAT_SYNC_GROUP); // Gone an hour after the cap, however many edits rewrote it. set_transient($key, array('first' => $first, 'wait' => (int) $scheduled), max(1, $first + 2 * $wait + HOUR_IN_SECONDS - time())); } } /** * Products with a change still waiting out the owner's delay. The full sync * leaves them alone: sending one now would skip its wait, and the wait is * capped, so it is sent soon anyway. */ function rayachat_waiting_ids() { if (!function_exists('as_get_scheduled_actions')) { return array(); } $actions = as_get_scheduled_actions(array( 'hook' => 'rayachat_sync_products', 'group' => RAYACHAT_SYNC_GROUP, 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => 1000, )); $waiting = array(); foreach ($actions as $action) { $args = $action->get_args(); foreach (isset($args[0]) && is_array($args[0]) ? $args[0] : array() as $id) { $waiting[(int) $id] = true; } } return $waiting; } add_action('woocommerce_new_product', 'rayachat_mark_product'); add_action('woocommerce_update_product', 'rayachat_mark_product'); add_action('woocommerce_update_product_variation', 'rayachat_mark_product'); add_action('woocommerce_updated_product_stock', 'rayachat_on_stock_written'); add_action('woocommerce_product_set_stock', 'rayachat_on_stock'); add_action('woocommerce_variation_set_stock', 'rayachat_on_stock'); add_action('woocommerce_product_set_stock_status', 'rayachat_mark_stock'); add_action('woocommerce_variation_set_stock_status', 'rayachat_mark_stock'); add_action('wp_trash_post', 'rayachat_on_delete'); add_action('before_delete_post', 'rayachat_on_delete'); add_action('shutdown', 'rayachat_flush_dirty');