Get a free advice now!

    Pick the topic

    Developer OutsourcingWeb developingApp developingDigital MarketingeCommerce systemseEntertainment systems

    Thank you for your message. It has been sent.

    Tags

    Nova Poshta – get package status using CRON

    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”

    CodeStatus
    1Nova Poshta is waiting for receipt from a sender
    2Deleted
    3Number not found
    4Shipment in the Sender’s city
    41Shipment in the Sender’s city (the status for local standard and local express services – delivery is within the city)
    5Shipment goes to the Recipient’s city
    6Shipment is in the Recipient’s city, an indicative delivery to the warehouse – XXX dd-mm. Expect an additional message about arrival.
    7, 8Arrived at the warehouse.
    9Shipment is received
    10Shipment 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.
    11Shipment is received %DateReceived%. The money transfer is given to the Recipient.
    14Shipment is transmitted to Recipient for checkup
    101On the way to the Recipient
    102, 103, 108Recipient’s refusal
    104Address changed
    105Storage is stopped
    106Express 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.

    Comments
    0 response

    Add comment

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

    Popular news

    How to Get and Use the ChatGPT API
    • Dev Tips and Tricks

    How to Get and Use the ChatGPT API

    April 25, 2024 by createIT
    eCommerce growth – is your business ready?
    • Services
    • Trends

    eCommerce growth – is your business ready?

    April 8, 2024 by createIT
    Digital marketing without third-party cookies – new rules
    • Technology
    • Trends

    Digital marketing without third-party cookies – new rules

    February 21, 2024 by createIT
    eCommerce healthcheck
    • Services
    • Trends

    eCommerce healthcheck

    January 24, 2024 by createIT
    Live Visitor Count in WooCommerce with SSE
    • Dev Tips and Tricks

    Live Visitor Count in WooCommerce with SSE

    December 12, 2023 by createIT
    Calculate shipping costs programmatically in WooCommerce
    • Dev Tips and Tricks

    Calculate shipping costs programmatically in WooCommerce

    December 11, 2023 by createIT
    Designing a cookie consent modal certified by TCF IAB
    • Dev Tips and Tricks

    Designing a cookie consent modal certified by TCF IAB

    December 7, 2023 by createIT

    Support – Tips and Tricks
    All tips in one place, and the database keeps growing. Stay up to date and optimize your work!

    Contact us