CreateIT
CreateIT
BLOG

Nova Poshta – get package status using CRON

Nova Poshta
TAGS: WooCommerce

Nova Poshta – get package status using CRON

SHARE

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.

Need help?

  • Looking for support from experienced programmers?

  • Need to fix a bug in the code?

  • Want to customize your webste/application?

ADD COMMENT

Your email address will not be published. Required fields are marked *

createIT Contact