PluginProbe
Route ‑ Shipping Protection / 2.4.13
Route ‑ Shipping Protection v2.4.13
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 / admin / class-routeapp-order-recover.php

class-routeapp-order-recover.php in Route ‑ Shipping Protection 2.4.13, at admin/class-routeapp-order-recover.php

355 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Routeapp_Order_Recover file.
4 *
5 * @package Routeapp\Admin
6 */
7
8 class Routeapp_Order_Recover {
9
10 public function __construct() {
11 // Ensure the AJAX actions are registered
12 add_action('wp_ajax_routeapp_save_orders', array($this, 'save'));
13 add_action('wp_ajax_routeapp_process_orders_batch', array($this, 'process_orders_batch'));
14
15 }
16
17 /**
18 * Save function to handle the processing of orders based on specified date range and batch size.
19 * This function processes orders in batches to avoid timeouts and server overload.
20 * It checks if the necessary POST parameters are set, fetches the orders in batches,
21 * processes each batch, and updates the order count accordingly.
22 */
23 public function save() {
24 if ($this->isPostRequestValid()) {
25 $recoverFrom = $_POST['routeapp_order_recover_from'];
26 $recoverTo = $_POST['routeapp_order_recover_to'];
27
28 // Get the total number of orders in the specified date range
29 $orderCount = $this->getOrderCount($recoverFrom, $recoverTo);
30
31 // Determine batch size based on the number of orders
32 $batchSize = $this->determineBatchSize($orderCount);
33
34 // Determine wait time based on batch size
35 $waitTime = $this->determineWaitTimeParameter($batchSize);
36
37 // Send back the initial response with the calculated values
38 wp_send_json_success(array(
39 'orderCount' => $orderCount,
40 'batchSize' => $batchSize,
41 'recoverFrom' => $recoverFrom,
42 'recoverTo' => $recoverTo,
43 'waitTime' => $waitTime,
44 ));
45 }
46 else {
47 wp_send_json_error(array(
48 'message' => __('Error', 'routeapp'),
49 ));
50 }
51 wp_die();
52 }
53
54 // New function to handle batch processing via AJAX
55 public function process_orders_batch() {
56
57 if ($this->isPostRequestValid()) {
58 $recoverFrom = $_POST['routeapp_order_recover_from'];
59 $recoverTo = $_POST['routeapp_order_recover_to'];
60 $batchSize = $_POST['batchSize'];
61 $offset = $_POST['offset'];
62
63 $orders = $this->getOrdersBatch($batchSize, $offset, $recoverTo, $recoverFrom);
64
65 if (!empty($orders)) {
66 $process_result = $this->processOrders($orders);
67 $response = array(
68 'processed' => count($orders),
69 );
70 if (!empty($process_result['tracking_warning'])) {
71 $response['tracking_warning'] = $process_result['tracking_warning'];
72 }
73 wp_send_json_success($response);
74 } else {
75 wp_send_json_error(array(
76 'message' => __('No more orders to process.', 'routeapp'),
77 ));
78 }
79 }
80 else {
81 wp_send_json_error(array(
82 'message' => __('Error', 'routeapp'),
83 ));
84 }
85 wp_die();
86 }
87
88 /**
89 * Validates the POST request to ensure required parameters are set.
90 *
91 * @return bool Returns true if the POST request contains 'routeapp_order_recover_from' and 'routeapp_order_recover_to'.
92 */
93 private function isPostRequestValid() {
94 return isset($_POST) && isset($_POST['routeapp_order_recover_from']) && isset($_POST['routeapp_order_recover_to']);
95 }
96
97
98 /**
99 * Fetches a batch of orders based on the specified batch size and offset.
100 *
101 * @param int $batchSize The number of orders to fetch in one batch.
102 * @param int $offset The starting point for fetching orders.
103 * @return array The fetched orders based on the specified arguments.
104 */
105 private function getOrdersBatch($batchSize, $offset, $recoverTo, $recoverFrom) {
106 $args = array(
107 'limit' => (int) $batchSize,
108 'offset' => (int) $offset,
109 'type' => 'shop_order',
110 'date_created' => $recoverFrom . '...' . $recoverTo,
111 );
112
113 return wc_get_orders( $args );
114 }
115
116 /**
117 * Processes the fetched orders by either updating order post meta or performing a massive order save operation.
118 * Optionally syncs tracking/shipments when the Sync tracking numbers checkbox is enabled.
119 *
120 * @param array $orders The fetched orders to be processed.
121 * @return array{
122 * tracking_warning?: string
123 * }
124 */
125 private function processOrders($orders) {
126 $result = array();
127
128 if (isset($_POST['routeapp_order_recover_reconcile_backend']) && $_POST['routeapp_order_recover_reconcile_backend']) {
129 $this->updateOrderPostMeta($orders);
130 } else {
131 $this->massiveOrderSave($orders);
132 }
133
134 if (isset($_POST['routeapp_order_recover_sync_tracking']) && $_POST['routeapp_order_recover_sync_tracking']) {
135 $tracking_warning = $this->syncOrderTrackings($orders);
136 if ($tracking_warning) {
137 $result['tracking_warning'] = $tracking_warning;
138 }
139 }
140
141 return $result;
142 }
143
144 /**
145 * Force-sync tracking numbers / shipments to Route for the given orders.
146 * Clears local shipment sync flags first so providers re-check Route and
147 * create missing shipments, then runs the original provider update paths.
148 *
149 * @param array $orders Orders to sync tracking for.
150 * @return string|null Warning message when tracking sync could not run, otherwise null.
151 */
152 private function syncOrderTrackings($orders) {
153 $routeapp = Routeapp_Cron_Schedules::get_route_public_instance();
154 if (!$routeapp) {
155 $warning = __( 'Tracking sync skipped: Route App instance is unavailable. Confirm the plugin is active and configured.', 'routeapp' );
156 error_log( '[routeapp] order_recover_sync_tracking: ' . $warning );
157 return $warning;
158 }
159
160 $shipment_tracking = new Routeapp_Shipment_Tracking();
161
162 foreach ($orders as $order) {
163 if (!$order) {
164 continue;
165 }
166
167 $order_id = $order->get_id();
168
169 try {
170 $this->clearLocalShipmentSyncMeta($order);
171 $shipment_tracking->update($order_id, $routeapp);
172 $shipment_tracking->parse_order_notes($order_id, $routeapp);
173 } catch (Exception $exception) {
174 // Keep processing the rest of the batch if one order fails.
175 $routeapp->routeapp_log($exception, array(
176 'endpoint' => 'order_recover_sync_tracking',
177 'params' => array(
178 'source_order_id' => $order_id,
179 ),
180 'method' => 'POST',
181 ));
182 continue;
183 }
184 }
185
186 return null;
187 }
188
189 /**
190 * Remove local meta that marks shipment sync as already completed.
191 * Needed to force a resync when Woo meta says success but Route has no shipment.
192 *
193 * @param \WC_Order $order Order to clear.
194 * @return void
195 */
196 private function clearLocalShipmentSyncMeta($order) {
197 $meta_keys = array(
198 'routeapp_shipment_api_called',
199 'routeapp_shipment_tracking_number',
200 'routeapp_shipment_tracking_provider',
201 'routeapp_shipment_cron_api_called',
202 );
203
204 if ( $this->is_hpos_enabled() ) {
205 $deleted = false;
206 foreach ( $meta_keys as $meta_key ) {
207 if ( $order->meta_exists( $meta_key ) ) {
208 $order->delete_meta_data( $meta_key );
209 $deleted = true;
210 }
211 }
212 if ( $deleted ) {
213 $order->save();
214 }
215 return;
216 }
217
218 foreach ( $meta_keys as $meta_key ) {
219 delete_post_meta( $order->get_id(), $meta_key );
220 }
221 }
222
223 /**
224 * Trigger save for all the selected orders, which will trigger the webhook for order.update
225 *
226 * @param $orders
227 * @return void
228 */
229 public function massiveOrderSave($orders) {
230 foreach ($orders as $order) {
231 Routeapp_Cron_Schedules::updateOrderStatusMetaIfNeeded( $order, false );
232 $order->save();
233 }
234 }
235
236 /**
237 * Update order post_meta with route order data
238 *
239 * @param $orders
240 * @return void
241 */
242 public function updateOrderPostMeta($orders) {
243 foreach ($orders as $order) {
244 Routeapp_Cron_Schedules::updateOrderStatusMetaIfNeeded( $order );
245
246 $has_route_meta = method_exists( $order, 'get_meta' )
247 ? $order->get_meta( '_routeapp_order_id' )
248 : get_post_meta( $order->get_id(), '_routeapp_order_id', true );
249 if ( ! $has_route_meta ) {
250 //check if order exists on Route side
251 $getOrderResponse = Routeapp_API_Client::getInstance()->get_order($order->get_id());
252 if (is_wp_error($getOrderResponse) || $getOrderResponse['response']['code'] != 200) {
253 continue;
254 }
255 Routeapp_Cron_Schedules::updateOrderPostMeta($order, $getOrderResponse);
256 }
257 }
258 }
259
260 /**
261 * Get the count of orders based on the specified date range.
262 * Supports both High-performance order storage (HPOS) and WordPress posts storage (legacy).
263 *
264 * @param string $from Starting date for the orders (YYYY-MM-DD).
265 * @param string $to Ending date for the orders (YYYY-MM-DD).
266 * @return int The count of orders.
267 */
268 public function getOrderCount($from, $to) {
269 global $wpdb;
270
271 $from_date = $from . ' 00:00:00';
272 $to_date = $to . ' 23:59:59';
273
274 if ( $this->is_hpos_enabled() ) {
275 // High-performance order storage: query wc_orders table
276 $orders_table = $wpdb->prefix . 'wc_orders';
277 $from_gmt = get_gmt_from_date( $from_date );
278 $to_gmt = get_gmt_from_date( $to_date );
279
280 $query = $wpdb->prepare(
281 "SELECT COUNT(1)
282 FROM {$orders_table} AS orders
283 WHERE orders.type = 'shop_order'
284 AND orders.status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
285 AND orders.date_created_gmt >= %s
286 AND orders.date_created_gmt <= %s",
287 $from_gmt,
288 $to_gmt
289 );
290 } else {
291 // WordPress posts storage (legacy): query wp_posts
292 $query = $wpdb->prepare(
293 "SELECT COUNT(1)
294 FROM {$wpdb->posts} AS posts
295 WHERE posts.post_type = 'shop_order'
296 AND posts.post_status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' )
297 AND posts.post_date >= %s
298 AND posts.post_date <= %s",
299 $from_date,
300 $to_date
301 );
302 }
303
304 return (int) $wpdb->get_var( $query );
305 }
306
307 /**
308 * Check if High-performance order storage (HPOS) is enabled.
309 *
310 * @return bool True if HPOS is enabled, false otherwise.
311 */
312 private function is_hpos_enabled() {
313 if ( ! class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) ) {
314 return false;
315 }
316 return \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
317 }
318
319 /**
320 * Determine batch size based on the number of orders.
321 *
322 * @param int $orderCount The number of orders.
323 * @return int The batch size.
324 */
325 private function determineBatchSize($orderCount) {
326 switch (true) {
327 case ($orderCount <= 1000):
328 return 100;
329 case ($orderCount <= 5000):
330 return 50;
331 case ($orderCount <= 10000):
332 return 25;
333 default:
334 return 10; // Fallback batch size for more than 100,000 orders
335 }
336 }
337
338 /**
339 * Determine wait time based on the batch size.
340 *
341 * @param int $batchSize The batch size.
342 * @return int The wait time.
343 */
344 private function determineWaitTimeParameter($batchSize) {
345 switch ($batchSize) {
346 case 100:
347 return 10;
348 case 50:
349 return 5;
350 default:
351 return 2; // Fallback wait time
352 }
353 }
354 }
355