Nova Poshta – get package status using CRON
CHALLENGE: we want to track what is happening with packages that are sent using the NovaPoshta Postal service
SOLUTION: create a CRON job that will use package IntDocNumber and ask NS API about current package status
Nova Poshta (Нова пошта) is a Ukrainian postal and courier company. It’s a popular service for delivering orders from e-shops or auction platforms (like Rozetka or prom). NS has over 6000 branches, postal-offices in the entire country. In Ukraine, it’s a convenient way for package delivery. You can open your package at the office desk, check the content, pay using cash and take the package.
Our WooCommerce shop checkout provides the ability for the user to pick one of the 6000 branches to deliver the package. After address delivery validation, the order is completed and the package is sent. In order postmeta, we’re saving ‘IntDocNumber’, which is the tracking number for the package.
Nova Poshta API
There is an API that can be used for package tracking. With the apiKey generated we can call the “getStatusDocuments” method using the model “TrackingDocument”. Having updated info about the package, we can inform our Shop Customer about “what is happening” with his order, or even provide the estimated time of delivery.
Cron Job
Our WordPress Cron Job will fetch all completed WooCommerce orders and ask the NovaPoshta API about the current “Document status”. It will be executed every hour. Our WP is using the Multisite environment, so we’re going to gather orders separately for each blog site. Internally, data will be fetched using WP REST API for WooCommerce. Originally, WordPress is running a separate CRON queue for every sub-site. Our function handles all sub-sites in one function, so we’re going to schedule it to run only for the main blog site.
/** * // functions.php * add action for checking NovaPoshta API package status */ add_action('ct_cron_ns_status_check1', 'ct_get_ns_packages_status'); // setup cron for NovaPoshta add_action('init', 'schedule_cron1'); function schedule_cron1(){ if ( ! is_main_site() ) { return; } if( !wp_next_scheduled( 'ct_cron_ns_status_check1' ) ) { wp_schedule_event( time(), 'hourly', 'ct_cron_ns_status_check1' ); } }
The main function will be triggering a “status check” for every sub-site and logging the total time of execution and statistics into the WP Data Logger custom table. This will help later in debugging or tracking API calls.
/** * Get NovaPoshta packages status */ const CT_SHOPS = array( array("name" => "UA", "blog_id" => 1), array("name" => "RU", "blog_id" => 2), ); function ct_get_ns_packages_status(){ $time_start = microtime(true); // UA first $blog_site_1[] = CT_SHOPS[0]; $count1 = ct_handle_single_site($blog_site_1); // then RU $blog_site_2[] = CT_SHOPS[1]; $count2 = ct_handle_single_site($blog_site_2); $time_end = microtime(true); $execution_time = ($time_end - $time_start)/60; // log response $tracking_data = array( 'total_time' => $execution_time, 'total_completed_orders' => $count1['total_completed_orders'] + $count2['total_completed_orders'], 'total_records_updates' => $count1['total_records_updates'] + $count2['total_records_updates'] ); do_action( 'ctlogger', $tracking_data, 'cronPackStatus', '' ); }
PHP helper function ct_handle_single_site() gathers all package numbers for completed WooCommerce orders, creating chunks of 50 items and calling NovaPoshta API. After a successful response, the woo order post meta is updated with the Status Code and Status values received from API.
function ct_handle_single_site($blog){ $orders_data = ct_get_package_numbers($blog); $total_records_updates = 0; /** * split array (50 packages per API call) */ $chunk_size = 50; foreach (array_chunk($orders_data['packages_numbers'], $chunk_size, true) as $packages_chunk) : $tracking_data = ct_ns_get_package_info($packages_chunk); // log response $log_key = 'getStatusDocuments_'. $blog[0]['name']; do_action( 'ctlogger', $tracking_data, $log_key, '' ); $data_for_sql_update = array(); if(empty($tracking_data['data'])){ return false; } // combine arrays : $orders_data['packages_numbers'] and $tracking_data foreach($tracking_data['data'] as $package){ $temp1 = array(); if(isset($packages_chunk[$package['Number']])){ $temp1['Number'] = isset($package['Number']) ? $package['Number'] : ''; $temp1['StatusCode'] = isset($package['StatusCode']) ? $package['StatusCode'] : ''; $temp1['Status'] = isset($package['Status']) ? $package['Status'] : ''; $order_number = $packages_chunk[$package['Number']]['order_number']; $data_for_sql_update[$order_number] = $temp1; } } // switch to subsite switch_to_blog($blog[0]['blog_id']); foreach($data_for_sql_update as $item_order_number => $item){ // UA/571/2021 $order_number_array = ctParseOrderNumber($item_order_number); if(! empty($order_number_array)){ $site_id = $order_number_array['site_id']; // additional caution if($site_id == $blog[0]['blog_id']){ $res1 = update_post_meta($order_number_array['order_id'], 'ctIntDocNumber', $item['Number']); $res2 = update_post_meta($order_number_array['order_id'], 'ctIntDocStatusCode', $item['StatusCode']); $res3 = update_post_meta($order_number_array['order_id'], 'ctIntDocStatus', $item['Status']); $res4 = update_post_meta($order_number_array['order_id'], 'ctIntDocRecentUpdate', time()); $total_records_updates++; } } } restore_current_blog(); endforeach; $res = array( 'total_records_updates' => $total_records_updates, 'total_completed_orders' => $orders_data['total_completed_orders'], 'packages_numbers' => $orders_data['packages_numbers'] ); return $res; }
Connecting to NovaPoshta API
This function is probably the most interesting one. It creates an API request to https://api.novaposhta.ua/v2.0/json/ . The method “getStatusDocuments” can handle up to 100 items in one request. Make sure to generate your own API Key and update the apiKey value in the code.
/** * @return mixed * "modelName": "TrackingDocument", * "calledMethod": "getStatusDocuments", * (up to 100 items in 1 request) */ function ct_ns_get_package_info($params){ // convert to api format $params = array_values($params); try { $data['modelName'] = 'TrackingDocument'; $data['calledMethod'] = 'getStatusDocuments'; $data['apiKey'] = 'abc123'; $data['methodProperties'] = [ 'Documents' => $params ]; $apiUrl = 'https://api.novaposhta.ua/v2.0/json/'; $body = wp_json_encode( $data); $options = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', ], 'timeout' => 60, 'redirection' => 5, 'blocking' => true, 'httpversion' => '1.0', 'sslverify' => false, 'data_format' => 'body', ]; $response = wp_remote_post( $apiUrl, $options ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); echo "Something went wrong: $error_message"; die(); } $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); if (isset($api_response['success']) && true === $api_response['success']) { return $api_response; } if(isset($api_response['statusCode'])){ die($api_response['message']); } return false; } catch (ApiServiceException $e) { return $this->jsonResponse([ 'success' => false, 'exception' => $e->getMessage() ]); } }
WooCommerce API – get all orders
To gather all package numbers that are added in post meta orders, we will use the WP_REST_Request() internal |WooCommerce API call. As parameter – status = completed is defined, which will query only orders that are already sent via the NovaPoshta courier.
/** * Woocommerce - get all completed orders for single sub-site * Woocommerce API (internal call) */ function ct_get_package_numbers($blog){ $blog = is_array($blog[0]) ? $blog[0] : $blog; ct_cron_authenticate_user(); switch_to_blog($blog["blog_id"]); $request = new WP_REST_Request( 'GET', '/wc/v3/orders' ); $request->set_query_params( [ 'status' => 'completed' ] ); $response = rest_do_request( $request ); $server = rest_get_server(); $completed_orders = $server->response_to_data( $response, false ); if(isset($completed_orders['code'])){ return new WP_REST_Response( $completed_orders, 401 ); } restore_current_blog(); $packages_numbers = array(); foreach ($completed_orders as $order) { // package number is stored in WooCommerce order meta $ttn_number = ''; foreach($order['meta_data'] as $metadata){ if($metadata->key == 'ctIntDocNumber') { $ttn_number = isset($metadata->value) ? $metadata->value : ''; break; } } if($ttn_number != ''){ $packages_numbers[$ttn_number] = [ 'DocumentNumber' => $ttn_number, 'Phone' => '', 'order_number' => $order['number'] ]; } } $response = array( 'total_completed_orders' => count($completed_orders), 'packages_numbers' => $packages_numbers ); return $response; }
It’s important to mention that WordPress Cron operates outside the wp_user scope. For a WP Rest API proper response, we need to authenticate as a user with administrator permissions (that can handle the multisite network area).
function ct_cron_authenticate_user(){ if ( defined( 'DOING_CRON' ) ){ // Notice - CRON job is running out of user context // To call internal API endpoint we will - hardcode user_id and authenticate the user // user_id = 1 = superadmin (with network privileges) wp_set_current_user ( 1 ); } }
Testing NovaPoshta CRON
The WP Cli command line is a useful tool that can be used for testing CRON jobs. By executing this single command, our function will be executed in the CRON context.
// test single cron job event wp cron event run ct_cron_ns_status_check1
Now, we can check WooCommerce order meta data. The Status and Status Code should be properly applied from the NovaPoshta API response. Data is stored in wp_postmeta and also available through the default WooCommerce API endpoint /wp-json/wc/v3/orders?status=completed
"meta_data": [ { "id": 3409, "key": "ctIntDocNumber", "value": "59000218530814" }, { "id": 3410, "key": "ctEstimatedDeliveryDate", "value": "03.03.2015" }, { "id": 3413, "key": "ctIntDocStatusCode", "value": "3" }, { "id": 3414, "key": "ctIntDocStatus", "value": "Номер не найден" }, { "id": 3417, "key": "ctIntDocRecentUpdate", "value": "1631883109" } ],
NovaPoshta tracking statuses
The status of shipment returned by the NS API is represented by a Status Code number and description Status. The list of all statuses can be found in the official API documentation: https://devcenter.novaposhta.ua/docs/services/556eef34a0fe4f02049c664e/operations/55702cbba0fe4f0cf4fc53ee
Here is a list of English translations of shipment statuses. Keep in mind that the list below may not be complete/can be outdated. My recommendation will be to refer to the original Ukrainian docs first.
List of possible code and status values (ENG)
https://api.novaposhta.ua/v2.0/
method “getStatusDocuments”
model “TrackingDocument”
Code | Status |
1 | Nova Poshta is waiting for receipt from a sender |
2 | Deleted |
3 | Number not found |
4 | Shipment in the Sender’s city |
41 | Shipment in the Sender’s city (the status for local standard and local express services – delivery is within the city) |
5 | Shipment goes to the Recipient’s city |
6 | Shipment is in the Recipient’s city, an indicative delivery to the warehouse – XXX dd-mm. Expect an additional message about arrival. |
7, 8 | Arrived at the warehouse. |
9 | Shipment is received |
10 | Shipment is received %DateReceived%. Within 24 hours, you will receive an SMS message about money transfer and will be able to receive it at the cash desk of the New Poshta warehouse. |
11 | Shipment is received %DateReceived%. The money transfer is given to the Recipient. |
14 | Shipment is transmitted to Recipient for checkup |
101 | On the way to the Recipient |
102, 103, 108 | Recipient’s refusal |
104 | Address changed |
105 | Storage is stopped |
106 | Express invoice of return delivery is received and created |
That’s it for today’s tutorial. Make sure to follow us for other useful tips and guidelines.