PluginProbe
Route ‑ Shipping Protection / 2.4.19
Route ‑ Shipping Protection v2.4.19
2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.2.4 2.2.5 2.2.6 2.2.8 2.2.9 2.3.0 2.3.1 All 153 releases
routeapp / includes / class-routeapp-cron-schedules.php

class-routeapp-cron-schedules.php in Route ‑ Shipping Protection 2.4.19, at includes/class-routeapp-cron-schedules.php

803 lines 29.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * WooCommerce Routeapp Cron Schedules
5 *
6 * @link https://route.com/
7 * @since 1.0.0
8 *
9 * @package Routeapp
10 * @subpackage Routeapp/includes
11 */
12
13 use Automattic\WooCommerce\Utilities\OrderUtil;
14
15 class Routeapp_Cron_Schedules
16 {
17
18 const RECONCILE_BATCH_SIZE = 100;
19 const RECONCILE_MAX_AGE_DAYS = 30;
20 const RECONCILE_LOCK_TTL = 900;
21 const RECONCILE_MAX_RUNTIME = 20;
22 const RECONCILE_MAX_BATCHES = 50;
23 const RECONCILE_CONTINUE_DELAY = 60;
24 const MISSING_ORDERS_LOCK_KEY = 'routeapp_missing_orders_lock';
25 const MISSING_SHIPMENTS_LOCK_KEY = 'routeapp_missing_shipments_lock';
26 const MISSING_ORDERS_CONTINUE_HOOK = 'routeapp_check_for_missing_orders_continue';
27 const MISSING_SHIPMENTS_CONTINUE_HOOK = 'routeapp_check_for_missing_shipments_continue';
28
29 private $routeapp_webhooks;
30
31 /**
32 * Initialize the class and set its properties.
33 *
34 * @since 1.0.0
35 */
36 public function __construct() {
37 $this->routeapp_webhooks = new Routeapp_Webhooks();
38 add_filter('cron_schedules', array($this, 'wpcron_schedule'));
39 add_action('routeapp_check_for_missing_orders', array($this, 'routeapp_validation_worker'));
40 add_action('routeapp_check_for_missing_shipments', array($this, 'routeapp_missing_shipments_worker'));
41 add_action('routeapp_check_for_invalid_webhooks', array($this, 'routeapp_webhooks_validator'));
42 add_action(self::MISSING_ORDERS_CONTINUE_HOOK, array($this, 'routeapp_validation_worker'));
43 add_action(self::MISSING_SHIPMENTS_CONTINUE_HOOK, array($this, 'routeapp_missing_shipments_worker'));
44 $this->init();
45 }
46
47 /**
48 * Add wp_cron Minutely and Biminutely Interval Schedules if not is scheduled
49 *
50 * @since 1.0.3
51 * @return void
52 */
53 public function init() {
54 $this->set_schedule_event('routeapp_daily', 'routeapp_check_for_missing_orders');
55 $this->set_schedule_event('routeapp_5_hours', 'routeapp_check_for_missing_shipments');
56 $this->set_schedule_event('routeapp_5_hours', 'routeapp_check_for_invalid_webhooks');
57 }
58
59 public function set_schedule_event($event, $action) {
60 if (!wp_next_scheduled($action)) {
61 wp_schedule_event(time(), $event, $action);
62 };
63 }
64
65 /**
66 * Orders processed per cron run. Capped to avoid unbounded load.
67 *
68 * @return int
69 */
70 public static function get_reconcile_batch_size() {
71 $size = (int) apply_filters('routeapp_reconcile_batch_size', self::RECONCILE_BATCH_SIZE);
72 if ($size < 1) {
73 return 1;
74 }
75 return min(self::RECONCILE_BATCH_SIZE, $size);
76 }
77
78 /**
79 * Seconds of work allowed per worker invocation.
80 *
81 * @return int
82 */
83 public static function get_reconcile_max_runtime() {
84 $seconds = (int) apply_filters('routeapp_reconcile_max_runtime', self::RECONCILE_MAX_RUNTIME);
85 if ( $seconds < 5 ) {
86 return 5;
87 }
88 return min(55, $seconds);
89 }
90
91 /**
92 * Max 100-order pages per invocation.
93 *
94 * @return int
95 */
96 public static function get_reconcile_max_batches() {
97 $batches = (int) apply_filters('routeapp_reconcile_max_batches', self::RECONCILE_MAX_BATCHES);
98 return $batches < 1 ? 1 : $batches;
99 }
100
101 /**
102 * Queue another worker run soon without replacing the daily/5-hour schedule.
103 *
104 * @param string $hook One-shot continue hook.
105 * @return void
106 */
107 public static function schedule_reconcile_continue($hook) {
108 if ( function_exists('wp_next_scheduled') && wp_next_scheduled($hook) ) {
109 return;
110 }
111 if ( function_exists('wp_schedule_single_event') ) {
112 wp_schedule_single_event(time() + self::RECONCILE_CONTINUE_DELAY, $hook);
113 }
114 }
115
116 /**
117 * Rolling window in days for reconciliation queries.
118 *
119 * @return int
120 */
121 public static function get_reconcile_max_age_days() {
122 $days = (int) apply_filters('routeapp_reconcile_max_age_days', self::RECONCILE_MAX_AGE_DAYS);
123 return $days < 1 ? self::RECONCILE_MAX_AGE_DAYS : $days;
124 }
125
126 /**
127 * Lower bound for reconcile queries: the later of plugin install date and the rolling window.
128 *
129 * @param int|null $now Unix timestamp to treat as now. Defaults to time().
130 * @return string Y-m-d
131 */
132 public static function get_reconcile_date_after( $now = null ) {
133 $now_ts = ( null === $now ) ? time() : (int) $now;
134 $window_ts = strtotime( '-' . self::get_reconcile_max_age_days() . ' days', $now_ts );
135 $window_start = self::format_site_date( $window_ts );
136
137 $install_ts = self::parse_reconcile_date_timestamp( get_option( '_routeapp_last_install_date' ) );
138 if ( ! $install_ts ) {
139 return $window_start;
140 }
141
142 return $install_ts > $window_ts
143 ? self::format_site_date( $install_ts )
144 : $window_start;
145 }
146
147 /**
148 * Format a unix timestamp as Y-m-d in the site timezone.
149 *
150 * @param int $timestamp Unix timestamp.
151 * @return string
152 */
153 public static function format_site_date( $timestamp ) {
154 if ( function_exists( 'wp_date' ) ) {
155 return wp_date( 'Y-m-d', $timestamp );
156 }
157 if ( function_exists( 'date_i18n' ) ) {
158 return date_i18n( 'Y-m-d', $timestamp );
159 }
160 return date( 'Y-m-d', $timestamp );
161 }
162
163 /**
164 * Parse an install-date option (Y-m-d or datetime) to a unix timestamp.
165 *
166 * @param mixed $value Option value.
167 * @return int
168 */
169 public static function parse_reconcile_date_timestamp( $value ) {
170 if ( ! is_string( $value ) || $value === '' ) {
171 return 0;
172 }
173 $timestamp = strtotime( $value );
174 return $timestamp ? (int) $timestamp : 0;
175 }
176
177 /**
178 * Persist order meta without firing order.updated / webhook delivery.
179 *
180 * update_meta_data() is in-memory only. save_meta_data() writes via the
181 * data store (add_meta/update_meta) and does not call data_store->update(),
182 * so it does not fire woocommerce_update_order. $order->save() does, and
183 * that is what queues Action Scheduler webhook jobs.
184 *
185 * @param \WC_Order $order Order object.
186 * @return void
187 */
188 public static function save_order_meta_only($order) {
189 if (!$order || !is_callable(array($order, 'save_meta_data'))) {
190 return;
191 }
192 $order->save_meta_data();
193 }
194
195 /**
196 * @return bool
197 */
198 public static function is_hpos_enabled() {
199 return class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
200 && OrderUtil::custom_orders_table_usage_is_enabled();
201 }
202
203 /**
204 * Acquire a cron lock with an atomic INSERT on wp_options (unique option_name).
205 * Expired locks are stolen only if CAST(option_value) < now (compare-and-swap).
206 *
207 * @param string $key Option name.
208 * @return bool
209 */
210 public static function acquire_cron_lock($key) {
211 global $wpdb;
212
213 if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) {
214 return false;
215 }
216
217 $now = time();
218 $expires = $now + self::RECONCILE_LOCK_TTL;
219
220 $result = $wpdb->query(
221 $wpdb->prepare(
222 "INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')
223 ON DUPLICATE KEY UPDATE option_value = IF(CAST(option_value AS UNSIGNED) < %d, VALUES(option_value), option_value)",
224 $key,
225 (string) $expires,
226 $now
227 )
228 );
229
230 if ( false === $result ) {
231 self::log_cron_lock( 'skipped cron run, lock query failed: ' . $key );
232 return false;
233 }
234
235 self::clear_cron_lock_option_cache( $key );
236
237 if ( (int) $wpdb->rows_affected < 1 ) {
238 self::log_cron_lock( 'skipped cron run, lock still held: ' . $key . ' (TTL ' . self::RECONCILE_LOCK_TTL . 's)' );
239 return false;
240 }
241
242 self::register_cron_lock_fatal_release($key);
243 return true;
244 }
245
246 /**
247 * @param string $key Option name.
248 * @return void
249 */
250 public static function release_cron_lock($key) {
251 delete_option($key);
252 }
253
254 /**
255 * Release the lock if a fatal error skips the worker's finally block.
256 *
257 * @param string $key Option name.
258 * @return void
259 */
260 public static function register_cron_lock_fatal_release($key) {
261 register_shutdown_function(function () use ($key) {
262 $error = error_get_last();
263 if ( ! $error || ! in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ), true ) ) {
264 return;
265 }
266 delete_option($key);
267 self::log_cron_lock( 'released cron lock after fatal: ' . $key . ' — ' . $error['message'] );
268 });
269 }
270
271 /**
272 * @param string $key Option name.
273 * @return void
274 */
275 public static function clear_cron_lock_option_cache($key) {
276 if ( ! function_exists( 'wp_cache_delete' ) ) {
277 return;
278 }
279 wp_cache_delete( $key, 'options' );
280 $notoptions = wp_cache_get( 'notoptions', 'options' );
281 if ( is_array( $notoptions ) && isset( $notoptions[ $key ] ) ) {
282 unset( $notoptions[ $key ] );
283 wp_cache_set( 'notoptions', $notoptions, 'options' );
284 }
285 }
286
287 /**
288 * @param string $message Log message.
289 * @return void
290 */
291 public static function log_cron_lock($message) {
292 if ( function_exists( 'error_log' ) ) {
293 error_log( '[routeapp] ' . $message );
294 }
295 }
296
297 /**
298 * @param $routeapp_client
299 * @param array $trackingPayload
300 * @param $order
301 * @param $params
302 * @return bool
303 */
304 public static function resend_shipment(array $trackingPayload)
305 {
306 $routeapp_client = Routeapp_API_Client::getInstance();
307
308 try {
309 $response = $routeapp_client->create_shipment($trackingPayload['tracking_number'], $trackingPayload);
310
311 return Routeapp_WooCommerce_Common_Tracking_Provider::is_successful_shipment_create_response($response);
312
313 } catch (Exception $exception) {
314 self::get_route_public_instance()->routeapp_log($exception, [
315 'endpoint' => 'shipments',
316 'params' => $trackingPayload,
317 'method' => 'POST'
318 ]);
319 return false;
320 }
321 }
322
323
324 /**
325 * Add wp_cron Daily, Daily Interval Schedules
326 *
327 * @since 1.0.3
328 * @param array $schedules
329 * @return array $schedules
330 */
331
332 public function wpcron_schedule($schedules) {
333
334 $schedules['routeapp_daily'] = array(
335 'interval' => 60 * 60 * 24,
336 'display' => __('Once Daily', 'routeapp')
337 );
338
339 $schedules['routeapp_5_hours'] = array(
340 'interval' => 60 * 60 * 5,
341 'display' => __('Every 5 hours', 'routeapp')
342 );
343
344 return $schedules;
345 }
346
347 /**
348 *
349 * Check if the order is created on Route API.
350 *
351 */
352 public static function has_route_order_id( $order_id ) {
353 return self::get_meta_data( $order_id, '_routeapp_order_id');
354 }
355
356 /**
357 * get meta data if custom order table is enabled
358 * @param $order_id
359 * @param $post_meta
360 * @return array|mixed|string
361 */
362 public static function get_meta_data( $order_id, $post_meta ) {
363 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
364 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
365 $order = wc_get_order($order_id);
366 return $order->get_meta($post_meta);
367 } else {
368 // Traditional CPT-based orders are in use.
369 return get_post_meta($order_id, $post_meta, true);
370 }
371 }
372
373
374 /**
375 *
376 * Get the Route Public class instance
377 *
378 */
379 public static function get_route_public_instance() {
380 global $routeapp_public;
381 return $routeapp_public;
382 }
383
384 /**
385 *
386 * Set the 'routeapp_shipment_cron_api_called' as a flag to avoid multiple requests to Route
387 *
388 */
389 public static function has_reconcile_shipping_triggered( $order_id ) {
390 return self::get_meta_data( $order_id, 'routeapp_shipment_cron_api_called') == 'success';
391 }
392
393 /**
394 *
395 * Get the 'routeapp_shipment_cron_api_called' as a flag see if the order has already be sent to Route
396 *
397 */
398 public static function set_reconcile_shipping_triggered( $order_id ) {
399 self::update_post_meta_value( $order_id, 'routeapp_shipment_cron_api_called', 'success' );
400 }
401
402 /**
403 * Update post meta value
404 *
405 * @param $order_id
406 * @param $post_meta
407 * @param $value
408 */
409 public static function update_post_meta_value( $order_id, $post_meta, $value) {
410 if ( self::is_hpos_enabled() ) {
411 $order = wc_get_order($order_id);
412 if ( ! $order ) {
413 return;
414 }
415 $order->update_meta_data($post_meta, $value );
416 self::save_order_meta_only( $order );
417 } else {
418 // Traditional CPT-based orders are in use.
419 update_post_meta( $order_id, $post_meta, $value );
420 }
421 }
422
423 /**
424 * Periodic Worker Triggering Missing Shippings on Route
425 *
426 * @return void
427 */
428 public static function routeapp_reconcile_shippings($orders)
429 {
430 $routeapp_client = Routeapp_API_Client::getInstance();
431 if (count($orders) > 0) {
432 foreach ($orders as $order) {
433
434 if (!$order || self::has_reconcile_shipping_triggered($order->get_id()) || !self::has_route_order_id($order->get_id())) {
435 continue;
436 }
437
438 try {
439
440 $trackingPayloads = self::format_shipping_data_api($order->get_id());
441 $tracking_numbers = array();
442 $create_failed = false;
443 foreach ($trackingPayloads as $trackingPayload){
444 $shipmentResponse = $routeapp_client->get_shipment($trackingPayload['tracking_number'], $order->get_id());
445 $tracking_numbers[] = $trackingPayload['tracking_number'];
446 if (is_wp_error($shipmentResponse) || (isset($shipmentResponse['response']['code']) && $shipmentResponse['response']['code'] == 200)) {
447 continue;
448 }
449
450 if ( ! self::resend_shipment($trackingPayload) ) {
451 $create_failed = true;
452 }
453 }
454
455 //save on post_meta the tracking ids
456 if (!empty($tracking_numbers)) {
457 $tracking_numbers = implode(Routeapp_WooCommerce_Tracking_Provider::SEPARATOR_PIPE, $tracking_numbers);
458 self::update_post_meta_value($order->get_id(), 'routeapp_shipment_tracking_number', $tracking_numbers);
459 }
460
461 // A 400 from create_shipment is not success — leave the cron
462 // flag unset so the next run can retry (EXT-425).
463 if ( ! $create_failed ) {
464 self::set_reconcile_shipping_triggered($order->get_id());
465 }
466
467 } catch (Exception $exception) {
468 continue;
469 }
470 }
471 }
472 }
473
474
475 /**
476 * create shipping data sending for API
477 * @since 1.0.0
478 * @param integer $order_id
479 */
480 public static function format_shipping_data_api($order_id)
481 {
482 $order = wc_get_order($order_id);
483
484 if (!$order) return [];
485
486 $tracking_number = self::get_meta_data( $order->get_id(), 'routeapp_shipment_tracking_number');
487 $routeapp_shipment_tracking = new Routeapp_Shipment_Tracking();
488 $courier_id = $routeapp_shipment_tracking->get_shipping_provider_name($order_id);
489 // Same ID resolution as shipment create / webhook order-create.
490 $product_ids = Routeapp_WooCommerce_Common_Tracking_Provider::get_order_products_from_order( $order );
491
492 if (strpos($tracking_number, Routeapp_WooCommerce_Tracking_Provider::SEPARATOR_PIPE) > 0) {
493 $tracking_numbers = explode(Routeapp_WooCommerce_Tracking_Provider::SEPARATOR_PIPE, $tracking_number);
494 } elseif (strpos($tracking_number, Routeapp_WooCommerce_Tracking_Provider::SEPARATOR_COMMA) > 0) {
495 $tracking_numbers = explode(Routeapp_WooCommerce_Tracking_Provider::SEPARATOR_COMMA, $tracking_number);
496 }else{
497 $tracking_numbers = [$tracking_number];
498 }
499
500 $trackings = [];
501
502 foreach ($tracking_numbers as $tracking){
503 if ($tracking!='') {
504 $trackings[] = [
505 'source_order_id' => $order->get_id(),
506 'source_product_ids' => $product_ids,
507 'courier_id' => is_string($courier_id) ? $courier_id : $courier_id[$tracking],
508 'tracking_number' => $tracking,
509 ];
510 }
511 }
512 if(empty($trackings[0])) {
513 //we don't have shipping on our post_meta yet, try to get the shipping data from third party module
514 $trackings = $routeapp_shipment_tracking->get_shipping_info($order_id);
515 }
516
517 return $trackings;
518 }
519
520
521 /**
522 * Periodic Worker Triggering Transaction Validation Jobs on Route
523 *
524 * @return void
525 */
526 function routeapp_validation_worker() {
527 if ( Routeapp_Webhooks::is_order_upsert_paused() ) {
528 self::log_cron_lock( 'skipping missing-order reconcile; RouteApp Order Upsert webhook is paused' );
529 return;
530 }
531
532 if ( ! self::acquire_cron_lock( self::MISSING_ORDERS_LOCK_KEY ) ) {
533 return;
534 }
535
536 try {
537 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
538 if ( ! is_array( $acceptedOrderStatuses ) || empty( $acceptedOrderStatuses ) ) {
539 return;
540 }
541
542 $deadline = time() + self::get_reconcile_max_runtime();
543 $max_batches = self::get_reconcile_max_batches();
544 $batch_size = self::get_reconcile_batch_size();
545 $processed_batches = 0;
546 $more = false;
547
548 do {
549 $args = array(
550 'type' => 'shop_order',
551 'limit' => $batch_size,
552 'status' => $acceptedOrderStatuses,
553 'date_created' => '>=' . self::get_reconcile_date_after(),
554 'orderby' => 'date',
555 'order' => 'DESC',
556 'return' => 'ids',
557 'meta_query' => array(
558 array(
559 'key' => '_routeapp_order_id',
560 'compare' => 'NOT EXISTS'
561 ),
562 )
563 );
564
565 $order_ids = wc_get_orders($args);
566 if ( ! is_array( $order_ids ) || empty( $order_ids ) ) {
567 $more = false;
568 break;
569 }
570
571 self::routeapp_run_order_reconcile( self::hydrate_orders( $order_ids ) );
572 $processed_batches++;
573 $more = count( $order_ids ) >= $batch_size;
574 } while ( $more && $processed_batches < $max_batches && time() < $deadline );
575
576 if ( $more ) {
577 self::schedule_reconcile_continue( self::MISSING_ORDERS_CONTINUE_HOOK );
578 }
579 } finally {
580 self::release_cron_lock( self::MISSING_ORDERS_LOCK_KEY );
581 }
582 }
583
584 /**
585 * Load WC_Order objects from IDs (or pass through existing objects).
586 *
587 * @param array $order_ids Order IDs or order objects.
588 * @return array
589 */
590 public static function hydrate_orders( $order_ids ) {
591 $orders = array();
592 if ( ! is_array( $order_ids ) ) {
593 return $orders;
594 }
595 foreach ( $order_ids as $order_id ) {
596 if ( is_object( $order_id ) && is_callable( array( $order_id, 'get_id' ) ) ) {
597 $orders[] = $order_id;
598 continue;
599 }
600 $order = wc_get_order( $order_id );
601 if ( $order ) {
602 $orders[] = $order;
603 }
604 }
605 return $orders;
606 }
607
608 /**
609 * Update order status meta (_routeapp_included_order_statuses, _routeapp_cancel_order_statuses)
610 * only if they don't exist or are different from current options.
611 *
612 * @param \WC_Order $order The order to update.
613 * @param bool $auto_save If true (default), persists meta when HPOS and meta changed
614 * without firing order.updated. Set to false when the caller
615 * will persist (e.g. persist_route_order_meta, massiveOrderSave).
616 * @return void
617 */
618 public static function updateOrderStatusMetaIfNeeded( $order, $auto_save = true ) {
619 if ( ! $order ) {
620 return;
621 }
622 $routeapp_public = self::get_route_public_instance();
623 if ( ! $routeapp_public ) {
624 return;
625 }
626 $is_hpos = self::is_hpos_enabled();
627 $acceptedOrderStatuses = get_option( 'routeapp_included_order_statuses' ) ? get_option( 'routeapp_included_order_statuses' ) : array();
628 $acceptedCanceledStatuses = get_option( 'routeapp_cancel_order_statuses' ) ? get_option( 'routeapp_cancel_order_statuses' ) : array();
629
630 $updated = false;
631 $updated |= $routeapp_public->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
632 $updated |= $routeapp_public->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );
633
634 if ( $updated && $is_hpos && $auto_save ) {
635 self::save_order_meta_only( $order );
636 }
637 }
638
639 /**
640 * Update order post_meta with route order data
641 *
642 * @param $order
643 * @param $routeOrder
644 * @return void
645 */
646 public static function updateOrderPostMeta($order, $routeOrder) {
647 //order was found, save route order id in db
648 $routeOrder = json_decode($routeOrder['body'], true);
649 $routeCharge = $routeOrder['insured_status'] == 'insured_selected' ? $routeOrder['paid_to_insure'] : '';
650 $protected = !empty($routeCharge) ? 1 : 0;
651 self::persist_route_order_meta( $order, $routeOrder['id'], $routeCharge, $protected );
652 }
653
654 /**
655 * Write Route order metadata without firing order.updated webhooks.
656 *
657 * @param \WC_Order $order Order object.
658 * @param mixed $route_order_id Route order id.
659 * @param mixed $route_charge Charge amount or empty.
660 * @param mixed $protected Protection flag.
661 * @return void
662 */
663 public static function persist_route_order_meta( $order, $route_order_id, $route_charge, $protected ) {
664 if ( ! $order ) {
665 return;
666 }
667 if ( self::is_hpos_enabled() ) {
668 $order->update_meta_data( '_routeapp_order_id', $route_order_id );
669 $order->update_meta_data( '_routeapp_route_charge', $route_charge );
670 $order->update_meta_data( '_routeapp_route_protection', $protected );
671 self::updateOrderStatusMetaIfNeeded( $order, false );
672 self::save_order_meta_only( $order );
673 return;
674 }
675 update_post_meta( $order->get_id(), '_routeapp_order_id', $route_order_id );
676 update_post_meta( $order->get_id(), '_routeapp_route_charge', $route_charge );
677 update_post_meta( $order->get_id(), '_routeapp_route_protection', $protected );
678 self::updateOrderStatusMetaIfNeeded( $order );
679 }
680
681 public static function routeapp_run_order_reconcile($orders) {
682 $routeapp_client = Routeapp_API_Client::getInstance();
683 if (count($orders) > 0) {
684
685 foreach ($orders as $order) {
686 //check if order exists on Route side
687 $getOrderResponse = $routeapp_client->get_order($order->get_id());
688
689 if (is_wp_error($getOrderResponse) || !in_array($getOrderResponse['response']['code'], [200, 404])) {
690 continue;
691 }
692
693 if ($getOrderResponse['response']['code'] == 200 ) {
694 //order was found, save route order id in db
695 self::updateOrderPostMeta($order, $getOrderResponse);
696 } else {
697 //order not found
698 //create from our side
699 $routeapp_public = self::get_route_public_instance();
700 if (!$routeapp_public->routeapp_is_shipping_method_allowed(false, $order)) continue;
701 $orderData = $routeapp_public->create_order_data_api($order->get_id());
702 $response = $routeapp_client->create_order($orderData);
703 if (!is_wp_error( $response )) {
704 $body = json_decode( $response['body'] );
705 try {
706 if ($response['response']['code'] > 201 && $response['response']['code'] < 409) {
707 $note = 'Route API Error while posting data. Errorcode- '.$response['response']['code'];
708 throw new Exception($note);
709 } else if (!empty($body->id) && !empty($body->order_number) || $response['response']['code'] == 409) {
710
711 $routeCharge = $body->insured_status == 'insured_selected' ? $body->paid_to_insure : '';
712 $protected = !empty($routeCharge) ? 1 : 0;
713 self::persist_route_order_meta( $order, $body->id, $routeCharge, $protected );
714 }
715 } catch(Exception $exception) {
716 $routeapp_public = self::get_route_public_instance();
717 $extraData = array(
718 'params' => $orderData,
719 'method' => 'POST',
720 'endpoint' => 'orders'
721 );
722 $routeapp_public->routeapp_log($exception, $extraData);
723 }
724 }
725 }
726 }
727 }
728 }
729
730 /**
731 * Periodic Worker that checks Route Webhooks validations
732 *
733 *
734 * @return void
735 */
736 function routeapp_webhooks_validator() {
737 $this->routeapp_webhooks->upsert_webhooks();
738 }
739
740 public function routeapp_missing_shipments_worker() {
741 if ( ! self::acquire_cron_lock( self::MISSING_SHIPMENTS_LOCK_KEY ) ) {
742 return;
743 }
744
745 try {
746 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
747 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
748 $statuses = array_merge(
749 is_array( $acceptedOrderStatuses ) ? $acceptedOrderStatuses : array(),
750 is_array( $acceptedCanceledStatuses ) ? $acceptedCanceledStatuses : array()
751 );
752 if ( empty( $statuses ) ) {
753 return;
754 }
755
756 $deadline = time() + self::get_reconcile_max_runtime();
757 $max_batches = self::get_reconcile_max_batches();
758 $batch_size = self::get_reconcile_batch_size();
759 $processed_batches = 0;
760 $more = false;
761
762 do {
763 $args = array(
764 'type' => 'shop_order',
765 'limit' => $batch_size,
766 'status' => $statuses,
767 'date_created' => '>=' . self::get_reconcile_date_after(),
768 'orderby' => 'date',
769 'order' => 'DESC',
770 'return' => 'ids',
771 'meta_query' => array(
772 array(
773 'key' => 'routeapp_shipment_tracking_number',
774 'value' => '',
775 'compare' => '!='
776 ),
777 array(
778 'key' => 'routeapp_shipment_cron_api_called',
779 'compare' => 'NOT EXISTS'
780 ),
781 )
782 );
783
784 $order_ids = wc_get_orders($args);
785 if ( ! is_array( $order_ids ) || empty( $order_ids ) ) {
786 $more = false;
787 break;
788 }
789
790 self::routeapp_reconcile_shippings( self::hydrate_orders( $order_ids ) );
791 $processed_batches++;
792 $more = count( $order_ids ) >= $batch_size;
793 } while ( $more && $processed_batches < $max_batches && time() < $deadline );
794
795 if ( $more ) {
796 self::schedule_reconcile_continue( self::MISSING_SHIPMENTS_CONTINUE_HOOK );
797 }
798 } finally {
799 self::release_cron_lock( self::MISSING_SHIPMENTS_LOCK_KEY );
800 }
801 }
802 }
803