# woo-postnl/4.0.1/includes/admin/class-wcpn-export.php

PostNL for WooCommerce, version 4.0.1. 1,549 lines.

- Page: https://pluginprobe.com/plugins/woo-postnl/4.0.1/code/includes/admin/class-wcpn-export.php
- Raw: https://pluginprobe.com/plugins/woo-postnl/4.0.1/raw/includes/admin/class-wcpn-export.php
- Modified: 2021-01-04T09:17:00+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/woo-postnl/4.0.1/code/includes/admin/class-wcpn-export.php#L10-L20`.

```php
<?php

use MyParcelNL\Sdk\src\Adapter\DeliveryOptions\AbstractDeliveryOptionsAdapter;
use MyParcelNL\Sdk\src\Exception\ApiException;
use MyParcelNL\Sdk\src\Exception\MissingFieldException;
use MyParcelNL\Sdk\src\Helper\MyParcelCollection;
use MyParcelNL\Sdk\src\Model\Consignment\AbstractConsignment;
use MyParcelNL\Sdk\src\Model\Consignment\PostNLConsignment;
use MyParcelNL\Sdk\src\Support\Arr;
use MyParcelNL\Sdk\src\Support\Str;
use WPO\WC\PostNL\Compatibility\Order as WCX_Order;
use WPO\WC\PostNL\Compatibility\WC_Core as WCX;
use WPO\WC\PostNL\Compatibility\WCPN_ChannelEngine_Compatibility as ChannelEngine;

if (! defined("ABSPATH")) {
    exit;
} // Exit if accessed directly

if (class_exists("WCPN_Export")) {
    return new WCPN_Export();
}

class WCPN_Export
{
    /**
     * @param WC_Order $order
     * @param string   $shippingMethodId
     *
     * @return int|null
     * @throws Exception
     */
    public function getOrderShippingClass(WC_Order $order, string $shippingMethodId = ''): ?int
    {
        if (empty($shippingMethodId)) {
            $orderShippingMethods = $order->get_items('shipping');

            if (! empty($orderShippingMethods)) {
                // we're taking the first (we're not handling multiple shipping methods as of yet)
                $orderShippingMethod = array_shift($orderShippingMethods);
                $shippingMethodId    = $orderShippingMethod['method_id'];
            } else {
                return null;
            }
        }

        $shippingMethod = $this->getShippingMethod($shippingMethodId);

        if (empty($shippingMethod)) {
            return null;
        }

        // get shipping classes from order
        $foundShippingClasses = $this->find_order_shipping_classes($order);

        $highest_class = $this->getShippingClass($shippingMethod, $foundShippingClasses);

        return $highest_class;
    }

    public const EXPORT = "wcpn_export";
    public const ADD_SHIPMENTS = "add_shipments";
    public const ADD_RETURN    = "add_return";
    public const GET_LABELS    = "get_labels";

    public const MODAL_DIALOG  = "modal_dialog";
    /**
     * Maximum characters length of item description.
     */
    public const ITEM_DESCRIPTION_MAX_LENGTH = 50;

    public const ORDER_DESCRIPTION_MAX_LENGTH = 45;
    public const DEFAULT_POSITIONS = [2, 4, 1, 3];

    public const SUFFIX_CHECK_REG  = "~^([a-z]{1}\d{1,3}|-\d{1,4}\d{2}\w{1,2}|[a-z]{1}[a-z\s]{0,3})(?:\W|$)~i";

    /**
     * Shipping methods that can never have delivery options.
     */
    public const DISALLOWED_SHIPPING_METHODS = [
        WCPN_Shipping_Methods::LOCAL_PICKUP,
    ];
    public $order_id;
    public $success;
    public $errors;

    public $postNLCollection;

    private $prefix_message;

    public function __construct()
    {
        $this->success = [];
        $this->errors  = [];

        require_once("class-wcpn-rest.php");
        require_once("class-wcpn-api.php");

        add_action("admin_notices", [$this, "admin_notices"]);

        add_action("wp_ajax_" . self::EXPORT, [$this, "export"]);
    }

    /**
     * @param int $orderId
     *
     * @throws ApiException
     * @throws ErrorException
     * @throws MissingFieldException
     */
    public function exportByOrderId(int $orderId): void
    {
        if ($orderId) {
            $export = new self();
            $export->addShipments([(string) $orderId], 0, false);
        }
    }

    /**
     * Get the value of a shipment option. Check if it was set manually, through the delivery options for example,
     *  if not get the value of the default export setting for given settingName.
     *
     * @param mixed  $option      Chosen value.
     * @param string $settingName Name of the setting to fall back to if there is no chosen value.
     *
     * @return mixed
     */
    public static function getChosenOrDefaultShipmentOption($option, string $settingName)
    {
        return $option ?? WCPOST()->setting_collection->getByName($settingName);
    }

    /**
     * @param $item
     * @param $order
     *
     * @return mixed|string
     */
    public static function get_item_display_name($item, $order)
    {
        // set base name
        $name = $item['name'];

        // add variation name if available
        $product = $order->get_product_from_item($item);
        if ($product && isset($item['variation_id']) && $item['variation_id'] > 0 && method_exists($product, 'get_variation_attributes')) {
            $name .= woocommerce_get_formatted_variation($product->get_variation_attributes());
        }

        return $name;
    }

    public function admin_notices()
    {
        // only do this when the user that initiated this
        if (isset($_GET["postnl_done"])) {
            $action_return = get_option("wcpostnl_admin_notices");
            $print_queue   = get_option("wcpostnl_print_queue", []);
            $error_notice = get_option("wcpostnl_admin_error_notices");

            if (! empty($action_return)) {
                foreach ($action_return as $type => $message) {
                    if (! in_array($type, ["success", "error"])) {
                        continue;
                    }

                    if ($type === "success" && ! empty($print_queue)) {
                        $print_queue_store = sprintf(
                            '<input type="hidden" value=\'%s\' class="wcpn__print-queue">',
                            json_encode(
                                [
                                    "shipment_ids" => $print_queue["shipment_ids"],
                                    "order_ids"    => $print_queue["order_ids"],
                                    "offset"       => $print_queue["offset"],
                                ]
                            )
                        );

                        // Empty queue
                        delete_option("wcpostnl_print_queue");
                    }

                    printf(
                        '<div class="wcpn__notice is-dismissible notice notice-%s"><p>%s</p>%s</div>',
                        $type,
                        $message,
                        $print_queue_store ?? ""
                    );
                }
                // destroy after reading
                delete_option("wcpostnl_admin_notices");
                wp_cache_delete("wcpostnl_admin_notices", "options");
            }
        }

        if (! empty($error_notice)) {
            printf(
                '<div class="wcpn__notice is-dismissible notice notice-error"><p>%s</p>%s</div>',
                $error_notice,
                $print_queue_store ?? ""
            );
            // destroy after reading
            delete_option("wcpostnl_admin_error_notices");
            wp_cache_delete("wcpostnl_admin_error_notices", "options");
        }

        if (isset($_GET["postnl"])) {
            switch ($_GET["postnl"]) {
                case "no_consignments":
                    $message = __(
                        "You have to export the orders to PostNL before you can print the labels!",
                        "woocommerce-postnl"
                    );
                    printf('<div class="wcpn__notice is-dismissible notice notice-error"><p>%s</p></div>', $message);
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * Export selected orders.
     *
     * @access public
     * @return void
     * @throws ApiException
     * @throws MissingFieldException
     * @throws Exception
     */
    public function export()
    {
        // Check the nonce
        if (! check_ajax_referer(WCPOST::NONCE_ACTION, "_wpnonce", false)) {
            die("Ajax security check failed. Did you pass a valid nonce in \$_REQUEST['_wpnonce']?");
        }

        if (! is_user_logged_in()) {
            wp_die(__("You do not have sufficient permissions to access this page.", "woocommerce-postnl"));
        }

        $return = [];

        // Check the user privileges (maybe use order ids for filter?)
        if (apply_filters(
            "wc_postnl_check_privs",
            ! current_user_can("manage_woocommerce_orders") && ! current_user_can("edit_shop_orders")
        )) {
            $return["error"] = __(
                "You do not have sufficient permissions to access this page.",
                "woocommerce-postnl"
            );
            echo json_encode($return);
            die();
        }

        $dialog  = $_REQUEST["dialog"] ?? null;
        $print   = $_REQUEST["print"] ?? null;
        $offset  = $_REQUEST["offset"] ?? 0;
        $request = $_REQUEST["request"];

        /**
         * @var $order_ids
         */
        $order_ids    = $this->sanitize_posted_array($_REQUEST["order_ids"] ?? []);
        $shipment_ids = $this->sanitize_posted_array($_REQUEST["shipment_ids"] ?? []);

        if (empty($shipment_ids) && empty($order_ids)) {
            $this->errors[] = __("You have not selected any orders!", "woocommerce-postnl");
        } else {
            try {
                switch ($request) {
                    // Creating consignments.
                    case self::ADD_SHIPMENTS:
                        $this->addShipments($order_ids, $offset, $print);
                        break;

                    // Creating a return shipment.
                    case self::ADD_RETURN:
                        if (empty($order_ids)) {
                            $this->errors[] = __("You have not selected any orders!", "woocommerce-postnl");
                            break;
                        }

                        $return = $this->addReturn($order_ids);
                        break;

                    // Downloading labels.
                    case self::GET_LABELS:
                        $return = $this->printLabels($order_ids, $shipment_ids, $offset);
                        break;

                    case self::MODAL_DIALOG:
                        $order_ids = $this->filterOrderDestinations($order_ids);
                        $this->modal_dialog($order_ids, $dialog);
                        break;
                }
            } catch (Exception $e) {
                $errorMessage = $e->getMessage();
                $this->errors[] = "$request: {$errorMessage}";
                add_option("wcpostnl_admin_error_notices", $errorMessage);
            }
        }

        // display errors directly if PDF requested or modal
        if (in_array($request, [self::ADD_RETURN, self::GET_LABELS, self::MODAL_DIALOG]) && ! empty($this->errors)) {
            echo $this->parse_errors($this->errors);
            die();
        }

        // format errors for html output
        if (! empty($this->errors)) {
            $return["error"] = $this->parse_errors($this->errors);
        }

        // if we're directed here from modal, show proper result page
        if (isset($_REQUEST["modal"])) {
            $this->modal_success_page($request, $return);
        } else {
            // return JSON response
            echo json_encode($return);
            die();
        }
    }

    /**
     * @param string|array $array
     *
     * @return array
     */
    public function sanitize_posted_array($array): array
    {
        if (is_array($array)) {
            return $array;
        }

        // check for JSON
        if (is_string($array) && strpos($array, "[") !== false) {
            $array = json_decode(stripslashes($array));
        }

        return (array) $array;
    }

    /**
     * @param $order_ids
     * @param $process
     *
     * @return array
     * @throws ApiException
     * @throws MissingFieldException
     * @throws ErrorException
     * @throws Exception
     */
    public function add_shipments(array $order_ids, bool $process)
    {
        $return          = [];
        $collection      = new MyParcelCollection();
        $processDirectly = WCPOST()->setting_collection->isEnabled(WCPOST_Settings::SETTING_PROCESS_DIRECTLY) || $process === true;

        WCPN_Log::add("*** Creating shipments started ***");

        /**
         * Loop over the order ids and create consignments for each order.
         */
        foreach ($order_ids as $order_id) {
            $order        = WCX::get_order($order_id);
            $consignment  = (new WCPN_Export_Consignments($order))->getConsignment();

            $extraOptions = WCX_Order::get_meta($order, WCPOST_Admin::META_SHIPMENT_OPTIONS_EXTRA);
            $colloAmount  = $extraOptions["collo_amount"] ?? 1;

            if ($colloAmount > 1) {
                $collection->addMultiCollo($consignment, $colloAmount);
            } else {
                $collection->addConsignment($consignment);
            }

            WCPN_Log::add("Shipment data for order {$order_id}.");
        }

        $collection = $collection->createConcepts();

        if ($processDirectly) {
            $collection->setLinkOfLabels();
        }

        foreach ($order_ids as $order_id) {
            $order          = WCX::get_order($order_id);
            $consignmentIds = ($collection->getConsignmentsByReferenceIdGroup($order_id))->getConsignmentIds();

            foreach ($consignmentIds as $consignmentId) {
                $shipment["shipment_id"] = $consignmentId;

                $this->saveShipmentData($order, $shipment);
                $this->updateOrderStatus($order);

                $this->success[$order_id] = $consignmentId;
            }

            if ($processDirectly) {
                $this->getShipmentData($consignmentIds, $order);
            }

            WCX_Order::update_meta_data(
                $order,
                WCPOST_Admin::META_LAST_SHIPMENT_IDS,
                $consignmentIds
            );
        }

        if (! empty($this->success)) {
            $return["success"]     = sprintf(
                __("%s shipments successfully prepare for PostNL", "woocommerce-postnl"),
                count($collection->getConsignmentIds())
            );
            $return["success_ids"] = $collection->getConsignmentIds();

            WCPN_Log::add($return["success"]);
            WCPN_Log::add("ids: " . implode(", ", $return["success_ids"]));
        }

        return $return;
    }

    /**
     * @param array $order_ids
     *
     * @return array
     * @throws Exception
     */
    public function addReturn(array $order_ids)
    {
        $return = [];

        WCPN_Log::add("*** Creating return shipments started ***");

        foreach ($order_ids as $order_id) {
            try {
                $return_shipments = [$this->prepareReturnShipmentData($order_id)];
                WCPN_Log::add("Return shipment data for order {$order_id}:", print_r($return_shipments, true));

                $api      = $this->init_api();
                $response = $api->add_shipments($return_shipments, "return");

                WCPN_Log::add("API response (order {$order_id}):\n" . print_r($response, true));

                $ids = Arr::get($response, "body.data.ids");

                if ($ids && ! empty($ids)) {
                    $order                    = WCX::get_order($order_id);
                    $ids                      = array_shift($response["body"]["data"]["ids"]);
                    $shipment_id              = $ids["id"];
                    $this->success[$order_id] = $shipment_id;

                    $shipment = [
                        "shipment_id" => $shipment_id,
                    ];

                    // save shipment data in order meta
                    $this->saveShipmentData($order, $shipment);
                } else {
                    WCPN_Log::add("\$response\[\"body.data.ids\"] empty or not found.", print_r($response, true));
                    throw new Exception("\$response\[\"body.data.ids\"] empty or not found.");
                }
            } catch (Exception $e) {
                $errorMessage = $e->getMessage();
                $this->errors[$order_id] = $errorMessage;
                add_option('wcpostnl_admin_error_notices', $errorMessage);
            }
        }

        return $return;
    }

    /**
     * @param array       $shipment_ids
     * @param array       $order_ids
     * @param int         $offset
     * @param string|null $displayOverride - Overrides display setting.
     *
     * @return array
     * @throws Exception
     */
    public function downloadOrGetUrlOfLabels(
        array $shipment_ids,
        array $order_ids = [],
        int $offset = 0,
        string $displayOverride = null
    ) {
        $return = [];

        WCPN_Log::add("*** downloadOrGetUrlOfLabels() ***");
        WCPN_Log::add("Shipment IDs: " . implode(", ", $shipment_ids));

        try {
            $api = $this->init_api();

            // positions are defined on landscape, but paper is filled portrait-wise
            $positions = array_slice(self::DEFAULT_POSITIONS, $offset % 4);

            $displaySetting = WCPOST()->setting_collection->getByName(WCPOST_Settings::SETTING_DOWNLOAD_DISPLAY);
            $display        = ($displayOverride ?? $displaySetting) === "display";
            $api->getShipmentLabels($shipment_ids, $order_ids, $positions, $display);
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
            add_option('wcpostnl_admin_error_notice', $e->getMessage());
        }

        return $return;
    }

    /**
     * @param array       $order_ids
     * @param int         $offset
     * @param string|null $display
     *
     * @return array
     * @throws Exception
     */
    public function getOrderLabels(array $order_ids, int $offset = 0, string $display = null)
    {
        $shipment_ids = $this->getShipmentIds($order_ids, ["only_last" => true]);

        if (empty($shipment_ids)) {
            WCPN_Log::add(" *** Failed label request(not exported yet) ***");

            throw new Exception(__(
                "The selected orders have not been exported to PostNL yet! ",
                "woocommerce-postnl"
            ));
        }

        return $this->downloadOrGetUrlOfLabels(
            $shipment_ids,
            $order_ids,
            $offset,
            $display
        );
    }

    /**
     * @param $order_ids
     */
    public function modal_dialog($order_ids, $dialog): void
    {
        // check for JSON
        if (is_string($order_ids) && strpos($order_ids, "[") !== false) {
            $order_ids = json_decode(stripslashes($order_ids));
        }

        // cast as array for single exports
        $order_ids = (array) $order_ids;
        require("views/html-bulk-options-form.php");
        die();
    }

    /**
     * @param $request
     * @param $result
     */
    public function modal_success_page($request, $result)
    {
        require("views/html-modal-result-page.php");
        die();
    }

    /**
     * @return WCPN_API
     * @throws Exception
     */
    public function init_api()
    {
        $key = $this->getSetting(WCPOST_Settings::SETTING_API_KEY);

        if (! ($key)) {
            throw new ErrorException(__("No API key found in PostNL settings", "woocommerce-postnl"));
        }

        return new WCPN_API($key);
    }

    /**
     * TODO: There are no options being passed right now but these will be necessary for NL.
     *
     * @param $order_id
     * @param $options
     *
     * @return array
     * @throws Exception
     */
    public function prepareReturnShipmentData($order_id, $options = [])
    {
        $order = WCX::get_order($order_id);

        $shipping_name =
            method_exists($order, "get_formatted_shipping_full_name") ? $order->get_formatted_shipping_full_name()
                : trim($order->get_shipping_first_name() . " " . $order->get_shipping_last_name());

        // set name & email
        $return_shipment_data = [
            "name"    => $shipping_name,
            "email"   => WCX_Order::get_prop($order, "billing_email"),
            "carrier" => PostNLConsignment::CARRIER_ID, // default to PostNL for now
        ];

        if (! Arr::get($return_shipment_data, "email")) {
            throw new Exception(__("No e-mail address found in order.", "woocommerce-postnl"));
        }

        // add options if available
        if (! empty($options)) {
            // convert insurance option
            if (! isset($options["insurance"]) && isset($options["insured_amount"])) {
                if ($options["insured_amount"] > 0) {
                    $options["insurance"] = [
                        "amount"   => (int) $options["insured_amount"] * 100,
                        "currency" => "EUR",
                    ];
                }
                unset($options["insured_amount"]);
                unset($options["insured"]);
            }
            // PREVENT ILLEGAL SETTINGS
            // convert numeric strings to int
            $int_options = ["package_type", "delivery_type", "signature", "return "];
            foreach ($options as $key => &$value) {
                if (in_array($key, $int_options)) {
                    $value = (int) $value;
                }
            }
            // remove frontend insurance option values
            if (isset($options["insured_amount"])) {
                unset($options["insured_amount"]);
            }
            if (isset($options["insured"])) {
                unset($options["insured"]);
            }
            $return_shipment_data["options"] = $options;
        }

        // get parent
        $shipment_ids = $this->getShipmentIds(
            (array) $order_id,
            [
                "exclude_concepts" => true,
                "only_last"        => true,
            ]
        );

        if (! empty($shipment_ids)) {
            $return_shipment_data["parent"] = (int) array_pop($shipment_ids);
        }

        return $return_shipment_data;
    }

    /**
     * @param WC_Order $order
     *
     * @return mixed|void
     * @throws Exception
     */
    public static function getRecipientFromOrder(WC_Order $order)
    {
        $isUsingPostNLFields = WCX_Order::has_meta($order, "_billing_street_name")
                                 && WCX_Order::has_meta($order, "_billing_house_number");

        $shipping_name =
            method_exists($order, "get_formatted_shipping_full_name") ? $order->get_formatted_shipping_full_name()
                : trim($order->get_shipping_first_name() . " " . $order->get_shipping_last_name());


        $connectPhone = WCPOST()->setting_collection->isEnabled(WCPOST_Settings::SETTING_CONNECT_PHONE);

        $address = [
            "cc"                     => (string) WCX_Order::get_prop($order, "shipping_country"),
            "city"                   => (string) WCX_Order::get_prop($order, "shipping_city"),
            "person"                 => $shipping_name,
            "company"                => (string) WCX_Order::get_prop($order, "shipping_company"),
            "email"                  => WCX_Order::get_prop($order, "billing_email") ?? "",
            "phone"                  => $connectPhone ? WCX_Order::get_prop($order, "billing_phone") : "",
            "street_additional_info" => WCX_Order::get_prop($order, "shipping_address_2"),
        ];

        $shipping_country = WCX_Order::get_prop($order, "shipping_country");
        if ($shipping_country === "NL") {
            // use billing address if old "pakjegemak" (1.5.6 and older)
            $pgAddress = WCX_Order::get_meta($order, WCPOST_Admin::META_PGADDRESS);

            if ($pgAddress) {
                $billing_name = method_exists($order, "get_formatted_billing_full_name")
                    ? $order->get_formatted_billing_full_name()
                    : trim(
                        $order->get_billing_first_name() . " " . $order->get_billing_last_name()
                    );
                $address_intl = [
                    "city"        => (string) WCX_Order::get_prop($order, "billing_city"),
                    "person"      => $billing_name,
                    "company"     => (string) WCX_Order::get_prop($order, "billing_company"),
                    "postal_code" => (string) WCX_Order::get_prop($order, "billing_postcode"),
                ];

                if ($isUsingPostNLFields) {
                    $address_intl["street"]        = (string) WCX_Order::get_meta($order, "_billing_street_name");
                    $address_intl["number"]        = (string) WCX_Order::get_meta($order, "_billing_house_number");
                    $address_intl["number_suffix"] =
                        (string) WCX_Order::get_meta($order, "_billing_house_number_suffix");
                } else {
                    // Split the address line 1 into three parts
                    preg_match(
                        WCPN_NL_Postcode_Fields::SPLIT_STREET_REGEX,
                        WCX_Order::get_prop($order, "billing_address_1"),
                        $address_parts
                    );
                    $address_intl["street"]                 = (string) $address_parts["street"];
                    $address_intl["number"]                 = (string) $address_parts["number"];
                    $address_intl["number_suffix"]          =
                        array_key_exists("number_suffix", $address_parts) // optional
                            ? (string) $address_parts["number_suffix"] : "";
                    $address_intl["street_additional_info"] = WCX_Order::get_prop($order, "billing_address_2");
                }
            } else {
                $address_intl = [
                    "postal_code" => (string) WCX_Order::get_prop($order, "shipping_postcode"),
                ];
                // If not using old fields
                if ($isUsingPostNLFields) {
                    $address_intl["street"]        = (string) WCX_Order::get_meta($order, "_shipping_street_name");
                    $address_intl["number"]        = (string) WCX_Order::get_meta($order, "_shipping_house_number");
                    $address_intl["number_suffix"] =
                        (string) WCX_Order::get_meta($order, "_shipping_house_number_suffix");
                } else {
                    // Split the address line 1 into three parts
                    preg_match(
                        WCPN_NL_Postcode_Fields::SPLIT_STREET_REGEX,
                        WCX_Order::get_prop($order, "shipping_address_1"),
                        $address_parts
                    );

                    $address_intl["street"]        = (string) $address_parts["street"];
                    $address_intl["number"]        = (string) $address_parts["number"];
                    $address_intl["number_suffix"] = (string) $address_parts["extension"] ?: "";

                    if (! $address_intl["number_suffix"]) {
                        if (preg_match(self::SUFFIX_CHECK_REG, $address["street_additional_info"])) {
                            $address_intl["number_suffix"]     = $address["street_additional_info"];
                            $address["street_additional_info"] = "";
                        }
                    }
                }
            }
        } else {
            $address_intl = [
                "postal_code"            => (string) WCX_Order::get_prop($order, "shipping_postcode"),
                "street"                 => (string) WCX_Order::get_prop($order, "shipping_address_1"),
                "street_additional_info" => (string) WCX_Order::get_prop($order, "shipping_address_2"),
                "region"                 => (string) WCX_Order::get_prop($order, "shipping_state"),
            ];
        }

        $address = array_merge($address, $address_intl);

        return apply_filters("wc_postnl_recipient", $address, $order);
    }

    /**
     * @param int $order_id
     * @param array $track_traces
     *
     * @internal param $shipment_ids
     */
    public static function addTrackTraceNoteToOrder(int $order_id, array $track_traces): void
    {
        if (! WCPOST()->setting_collection->isEnabled(WCPOST_Settings::SETTING_BARCODE_IN_NOTE)) {
            return;
        }

        $prefix_message = WCPOST()->setting_collection->getByName(WCPOST_Settings::SETTING_BARCODE_IN_NOTE_TITLE);

        // Select the barcode text of the PostNL settings
        $prefix_message = $prefix_message ? $prefix_message . " " : "";

        $order = WCX::get_order($order_id);
        $order->add_order_note($prefix_message . implode(", ", $track_traces));
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    private function getSetting(string $name)
    {
        return WCPOST()->setting_collection->getByName($name);
    }

    /**
     * @param array $order_ids
     * @param array $args
     *
     * @return array
     */
    public function getShipmentIds(array $order_ids, array $args): array
    {
        $shipment_ids = [];

        foreach ($order_ids as $order_id) {
            $order           = WCX::get_order($order_id);
            $order_shipments = WCX_Order::get_meta($order, WCPOST_Admin::META_SHIPMENTS);

            if (empty($order_shipments)) {
                continue;
            }

            $order_shipment_ids = [];
            // exclude concepts or only concepts
            foreach ($order_shipments as $shipment_id => $shipment) {
                if (isset($args["exclude_concepts"]) && empty($shipment["track_trace"])) {
                    continue;
                }
                if (isset($args["only_concepts"]) && ! empty($shipment["track_trace"])) {
                    continue;
                }

                $order_shipment_ids[] = $shipment_id;
            }

            if (isset($args["only_last"])) {
                $last_shipment_ids = WCX_Order::get_meta($order, WCPOST_Admin::META_LAST_SHIPMENT_IDS);

                if (! empty($last_shipment_ids) && is_array($last_shipment_ids)) {
                    foreach ($order_shipment_ids as $order_shipment_id) {
                        if (in_array($order_shipment_id, $last_shipment_ids)) {
                            $shipment_ids[] = $order_shipment_id;
                        }
                    }
                } else {
                    $shipment_ids[] = array_pop($order_shipment_ids);
                }
            } else {
                $shipment_ids[] = array_merge($shipment_ids, $order_shipment_ids);
            }
        }

        return $shipment_ids;
    }

    /**
     * @param WC_Order $order
     * @param array    $shipment
     *
     * @return void
     * @throws Exception
     */
    public function saveShipmentData(WC_Order $order, array $shipment): void
    {
        if (empty($shipment)) {
            throw new Exception("save_shipment_data requires a valid \$shipment.");
        }

        $old_shipments                           = [];
        $new_shipments                           = [];
        $new_shipments[$shipment["shipment_id"]] = $shipment;

        if (WCX_Order::has_meta($order, WCPOST_Admin::META_SHIPMENTS)) {
            $old_shipments = WCX_Order::get_meta($order, WCPOST_Admin::META_SHIPMENTS);
        }

        $new_shipments = array_replace_recursive($old_shipments, $new_shipments);

        WCX_Order::update_meta_data($order, WCPOST_Admin::META_SHIPMENTS, $new_shipments);
    }

    /**
     * Determine appropriate package type for this order.
     *
     * @param WC_Order                            $order
     * @param AbstractDeliveryOptionsAdapter|null $deliveryOptions
     *
     * @return string
     * @throws Exception
     */
    public function getPackageTypeFromOrder(
        WC_Order $order,
        AbstractDeliveryOptionsAdapter $deliveryOptions = null
    ): string {
        $packageTypeFromDeliveryOptions = $deliveryOptions
            ? $deliveryOptions->getPackageType()
            : null;

        if ($packageTypeFromDeliveryOptions) {
            return $packageTypeFromDeliveryOptions;
        }

        // Get pre 4.0.0 package type if it exists.
        if (WCX_Order::has_meta($order, WCPOST_Admin::META_SHIPMENT_OPTIONS_LT_4_0_0)) {
            $shipmentOptions = WCX_Order::get_meta($order, WCPOST_Admin::META_SHIPMENT_OPTIONS_LT_4_0_0);
            return self::getPackageTypeAsString($shipmentOptions['package_type']);
        }

        $packageType = AbstractConsignment::DEFAULT_PACKAGE_TYPE_NAME;

        // get shipping methods from order
        $orderShippingMethods = $order->get_items('shipping');

        if (! empty($orderShippingMethods)) {
            // we're taking the first (we're not handling multiple shipping methods as of yet)
            $orderShippingMethod = array_shift($orderShippingMethods);
            $orderShippingMethod = $orderShippingMethod['method_id'];

            $orderShippingClass = WCX_Order::get_meta($order, WCPOST_Admin::META_HIGHEST_SHIPPING_CLASS);
            if (empty($orderShippingClass)) {
                $orderShippingClass = $this->getOrderShippingClass($order, $orderShippingMethod);
            }

            $packageType = self::getPackageTypeFromShippingMethod(
                $orderShippingMethod,
                $orderShippingClass
            );
        }

        return $packageType;
    }

    /**
     * @param $shippingMethod
     * @param $shippingClass
     *
     * @return string
     */
    public static function getPackageTypeFromShippingMethod($shippingMethod, $shippingClass): string
    {
        $packageType           = AbstractConsignment::PACKAGE_TYPE_PACKAGE_NAME;
        $shippingMethodIdClass = "";

        if (Str::startsWith($shippingMethod, 'table_rate:') && class_exists('WC_Table_Rate_Shipping')) {
            // Automattic / WooCommerce table rate
            // use full method = method_id:instance_id:rate_id
            $shippingMethodId = $shippingMethod;
        } else {
            // non table rates
            if (Str::contains($shippingMethodIdClass, ':')) {
                // means we have method_id:instance_id
                $shippingMethod   = explode(':', $shippingMethod);
                $shippingMethodId = $shippingMethod[0];
            } else {
                $shippingMethodId = $shippingMethod;
            }

            // add class if we have one
            if (! empty($shippingClass)) {
                $shippingMethodIdClass = "{$shippingMethodId}:{$shippingClass}";
            }
        }

        $packageTypes = WCPOST()->setting_collection->getByName(WCPOST_Settings::SETTING_SHIPPING_METHODS_PACKAGE_TYPES);
        foreach ($packageTypes as $packageTypeKey => $packageTypeShippingMethods) {
            if (self::isActiveMethod(
                $shippingMethodId,
                $packageTypeShippingMethods,
                $shippingMethodIdClass,
                $shippingClass
            )) {
                $packageType = $packageTypeKey;
                break;
            }
        }

        return self::getPackageTypeAsString($packageType);
    }

    /**
     * @param string|null $packageType
     *
     * @return string
     */
    public static function getPackageTypeHuman(?string $packageType): string
    {
        if ($packageType) {
            $packageType = WCPN_Data::getPackageTypeHuman($packageType);
        }

        return $packageType ?? __("Unknown", "woocommerce-postnl");
    }

    /**
     * Will convert any package type to a valid string package type.
     *
     * @param mixed $packageType
     *
     * @return string
     */
    public static function getPackageTypeAsString($packageType): string
    {
        if (is_numeric($packageType)) {
            $packageType = WCPN_Data::getPackageTypeName($packageType);
        }

        if (! is_string($packageType) || ! in_array($packageType, WCPN_Data::getPackageTypes())) {
            // Log data when this occurs but don't actually throw an exception.
            $type = gettype($packageType);
            WCPN_Log::add(new Exception("Tried to convert invalid value to package type: $packageType ($type)"));

            $packageType = null;
        }

        return $packageType ?? AbstractConsignment::DEFAULT_PACKAGE_TYPE_NAME;
    }

    /**
     * @param $errors
     *
     * @return mixed|string
     */
    public function parse_errors($errors)
    {
        $parsed_errors = [];

        foreach ($errors as $key => $error) {
            // check if we have an order_id
            if ($key > 10) {
                $parsed_errors[] = sprintf(
                    "<strong>%s %s:</strong> %s",
                    __("Order", "woocommerce-postnl"),
                    $key,
                    $error
                );
            } else {
                $parsed_errors[] = $error;
            }
        }

        if (count($parsed_errors) == 1) {
            $html = array_shift($parsed_errors);
        } else {
            foreach ($parsed_errors as &$parsed_error) {
                $parsed_error = "<li>{$parsed_error}</li>";
            }
            $html = sprintf("<ul>%s</ul>", implode("\n", $parsed_errors));
        }

        return $html;
    }

    public function getShipmentStatusName($status_code)
    {
        $shipment_statuses = [
            1  => __("pending - concept", "woocommerce-postnl"),
            2  => __("pending - registered", "woocommerce-postnl"),
            3  => __("enroute - handed to carrier", "woocommerce-postnl"),
            4  => __("enroute - sorting", "woocommerce-postnl"),
            5  => __("enroute - distribution", "woocommerce-postnl"),
            6  => __("enroute - customs", "woocommerce-postnl"),
            7  => __("delivered - at recipient", "woocommerce-postnl"),
            8  => __("delivered - ready for pickup", "woocommerce-postnl"),
            9  => __("delivered - package picked up", "woocommerce-postnl"),
            30 => __("inactive - concept", "woocommerce-postnl"),
            31 => __("inactive - registered", "woocommerce-postnl"),
            32 => __("inactive - enroute - handed to carrier", "woocommerce-postnl"),
            33 => __("inactive - enroute - sorting", "woocommerce-postnl"),
            34 => __("inactive - enroute - distribution", "woocommerce-postnl"),
            35 => __("inactive - enroute - customs", "woocommerce-postnl"),
            36 => __("inactive - delivered - at recipient", "woocommerce-postnl"),
            37 => __("inactive - delivered - ready for pickup", "woocommerce-postnl"),
            38 => __("inactive - delivered - package picked up", "woocommerce-postnl"),
            99 => __("inactive - unknown", "woocommerce-postnl"),
        ];

        if (isset($shipment_statuses[$status_code])) {
            return $shipment_statuses[$status_code];
        } else {
            return __("Unknown status", "woocommerce-postnl");
        }
    }

    /**
     * Retrieves, updates and returns shipment data for given id.
     *
     * @param array    $ids
     * @param WC_Order $order
     *
     * @return array
     * @throws Exception
     */
    public function getShipmentData(array $ids, WC_Order $order): array
    {
        $data     = [];
        $api      = $this->init_api();
        $response = $api->get_shipments($ids);

        $shipments = Arr::get($response, "body.data.shipments");

        if (! $shipments) {
            return [];
        }

        foreach ($shipments as $shipment) {
            if (! isset($shipment["id"])) {
                return [];
            }

            // if shipment id matches and status is not concept, get track trace barcode and status name
            $status        = $this->getShipmentStatusName($shipment["status"]);
            $track_trace   = $shipment["barcode"];
            $shipment_id   = $shipment["id"];
            $shipment_data = compact("shipment_id", "status", "track_trace", "shipment");
            $this->saveShipmentData($order, $shipment_data);

            ChannelEngine::updateMetaOnExport($order, $track_trace);

            $data[$shipment_id] = $shipment_data;
        }

        return $data;
    }

    /**
     * @param $item
     * @param $order
     *
     * @return float
     */
    public static function getItemWeightKg($item, WC_Order $order): float
    {
        $product = $order->get_product_from_item($item);

        if (empty($product)) {
            return 0;
        }

        $weight        = (float) $product->get_weight();
        $productWeight = WCPN_Export::calculatedKiloWeight($weight);

        return  $productWeight * (int) $item["qty"];
    }

    /**
     * @param float $weight
     *
     * @return float
     */
    public static function calculatedKiloWeight(float $weight): float
    {
        $weightUnit = get_option("woocommerce_weight_unit");
        switch ($weightUnit) {
            case "g":
                $calculatedWeight = $weight / 1000;
                break;
            case "lbs":
                $calculatedWeight = $weight * 0.45359237;
                break;
            case "oz":
                $calculatedWeight = $weight * 0.0283495231;
                break;
            default:
                $calculatedWeight = $weight;
                break;
        }

        return $calculatedWeight;
    }

    /**
     * @param $chosenMethod
     *
     * @throws \Exception
     */
    public static function getShippingMethod(string $chosenMethod)
    {
        if (version_compare(WOOCOMMERCE_VERSION, "2.6", "<") || $chosenMethod ===
            WCPN_Shipping_Methods::LEGACY_FLAT_RATE) {
            return self::getLegacyShippingMethod($chosenMethod);
        }

        [$methodSlug, $methodInstance] = WCPN_Checkout::splitShippingMethodString($chosenMethod);

        $isDisallowedShippingMethod = in_array($methodSlug, self::DISALLOWED_SHIPPING_METHODS);
        $isManualOrder              = empty($methodInstance);

        if ($isDisallowedShippingMethod || $isManualOrder) {
            return null;
        }

        return WC_Shipping_Zones::get_shipping_method($methodInstance) ?? null;
    }

    /**
     * @param string $chosen_method
     *
     * @return null|WC_Shipping_Method
     */
    private static function getLegacyShippingMethod(string $chosen_method): ?WC_Shipping_Method
    {
        // only for flat rate or legacy flat rate
        if (! in_array(
            $chosen_method,
            [
                WCPN_Shipping_Methods::FLAT_RATE,
                WCPN_Shipping_Methods::LEGACY_FLAT_RATE,
            ]
        )) {
            return null;
        }

        $shipping_methods = WC()->shipping()->load_shipping_methods();

        if (! isset($shipping_methods[$chosen_method])) {
            return null;
        }

        return $shipping_methods[$chosen_method];
    }

    /**
     * @param $shipping_method
     * @param $found_shipping_classes
     *
     * @return int|null
     */
    public function getShippingClass($shipping_method, $found_shipping_classes): ?int
    {
        // get most expensive class
        // adapted from $shipping_method->calculate_shipping()
        $highest_class_cost = 0;
        $highest_class      = null;
        foreach ($found_shipping_classes as $shipping_class => $products) {
            // Also handles BW compatibility when slugs were used instead of ids
            $shipping_class_term    = get_term_by("slug", $shipping_class, "product_shipping_class");
            $shipping_class_term_id = "";

            if ($shipping_class_term != null) {
                $shipping_class_term_id = $shipping_class_term->term_id;
            }

            $class_cost_string = $shipping_class_term && $shipping_class_term_id ? $shipping_method->get_option(
                "class_cost_" . $shipping_class_term_id,
                $shipping_method->get_option("class_cost_" . $shipping_class, "")
            ) : $shipping_method->get_option("no_class_cost", "");

            if ($class_cost_string === "") {
                continue;
            }

            $class_cost = $this->wc_flat_rate_evaluate_cost(
                $class_cost_string,
                [
                    "qty"  => array_sum(wp_list_pluck($products, "quantity")),
                    "cost" => array_sum(wp_list_pluck($products, "line_total")),
                ],
                $shipping_method
            );
            if ($class_cost > $highest_class_cost && ! empty($shipping_class_term_id)) {
                $highest_class_cost = $class_cost;
                $highest_class      = $shipping_class_term->term_id;
            }
        }

        return $highest_class;
    }

    /**
     * Adapted from WC_Shipping_Flat_Rate - Protected method
     * Evaluate a cost from a sum/string.
     *
     * @param string $sum
     * @param array  $args
     * @param        $flat_rate_method
     *
     * @return string
     */
    public function wc_flat_rate_evaluate_cost(string $sum, array $args, $flat_rate_method)
    {
        if (version_compare(WOOCOMMERCE_VERSION, "2.6", ">=")) {
            include_once(WC()->plugin_path() . "/includes/libraries/class-wc-eval-math.php");
        } else {
            include_once(WC()->plugin_path() . "/includes/shipping/flat-rate/includes/class-wc-eval-math.php");
        }

        // Allow 3rd parties to process shipping cost arguments
        $args           = apply_filters("woocommerce_evaluate_shipping_cost_args", $args, $sum, $flat_rate_method);
        $locale         = localeconv();
        $decimals       = [
            wc_get_price_decimal_separator(),
            $locale["decimal_point"],
            $locale["mon_decimal_point"],
            ",",
        ];
        $this->fee_cost = $args["cost"];

        // Expand shortcodes
        add_shortcode("fee", [$this, "wc_flat_rate_fee"]);

        $sum = do_shortcode(
            str_replace(
                ["[qty]", "[cost]"],
                [$args["qty"], $args["cost"]],
                $sum
            )
        );

        remove_shortcode("fee");

        // Remove whitespace from string
        $sum = preg_replace("/\s+/", "", $sum);

        // Remove locale from string
        $sum = str_replace($decimals, ".", $sum);

        // Trim invalid start/end characters
        $sum = rtrim(ltrim($sum, "\t\n\r\0\x0B+*/"), "\t\n\r\0\x0B+-*/");

        // Do the math
        return $sum ? WC_Eval_Math::evaluate($sum) : 0;
    }

    /**
     * Adapted from WC_Shipping_Flat_Rate - Protected method
     * Work out fee (shortcode).
     *
     * @param array $atts
     *
     * @return string
     */
    public function wc_flat_rate_fee($atts)
    {
        $atts = shortcode_atts(
            [
                "percent" => "",
                "min_fee" => "",
                "max_fee" => "",
            ],
            $atts
        );

        $calculated_fee = 0;

        if ($atts["percent"]) {
            $calculated_fee = $this->fee_cost * (floatval($atts["percent"]) / 100);
        }

        if ($atts["min_fee"] && $calculated_fee < $atts["min_fee"]) {
            $calculated_fee = $atts["min_fee"];
        }

        if ($atts["max_fee"] && $calculated_fee > $atts["max_fee"]) {
            $calculated_fee = $atts["max_fee"];
        }

        return $calculated_fee;
    }

    /**
     * Filter out orders shipping to country codes that are not in the allowed list.
     *
     * @param $order_ids
     *
     * @return mixed
     * @throws Exception
     */
    public function filterOrderDestinations(array $order_ids): array
    {
        foreach ($order_ids as $key => $order_id) {
            $order            = WCX::get_order($order_id);
            $shipping_country = WCX_Order::get_prop($order, "shipping_country");

            if (! WCPN_Country_Codes::isAllowedDestination($shipping_country)) {
                unset($order_ids[$key]);
            }
        }

        return $order_ids;
    }

    /**
     * @param $shipment_id
     *
     * @return mixed
     * @throws ErrorException
     * @throws Exception
     */
    private function getShipmentBarcodeFromApi($shipment_id)
    {
        $api      = $this->init_api();
        $response = $api->get_shipments($shipment_id);

        if (! isset($response["body"]["data"]["shipments"][0]["barcode"])) {
            throw new ErrorException("No PostNL barcode found for shipment id; " . $shipment_id);
        }

        return $response["body"]["data"]["shipments"][0]["barcode"];
    }

    /**
     * @param $shipping_method_id
     * @param $package_type_shipping_methods
     * @param $shipping_method_id_class
     * @param $shipping_class
     *
     * @return bool
     */
    private static function isActiveMethod(
        $shipping_method_id,
        $package_type_shipping_methods,
        $shipping_method_id_class,
        $shipping_class
    ) {
        //support WooCommerce flat rate
        // check if we have a match with the predefined methods
        if (in_array($shipping_method_id, $package_type_shipping_methods)) {
            return true;
        }

        if (in_array($shipping_method_id_class, $package_type_shipping_methods)) {
            return true;
        }

        // fallback to bare method (without class) (if bare method also defined in settings)
        if (! empty($shipping_method_id_class)
            && in_array(
                $shipping_method_id_class,
                $package_type_shipping_methods
            )) {
            return true;
        }

        // support WooCommerce Table Rate Shipping by WooCommerce
        if (! empty($shipping_class) && in_array($shipping_class, $package_type_shipping_methods)) {
            return true;
        }

        // support WooCommerce Table Rate Shipping by Bolder Elements
        $newShippingClass = str_replace(":", "_", $shipping_class);
        if (! empty($shipping_class) && in_array($newShippingClass, $package_type_shipping_methods)) {
            return true;
        }

        return false;
    }

    /**
     * Update the status of given order based on the automatic order status settings.
     *
     * @param WC_Order $order
     */
    private function updateOrderStatus(WC_Order $order): void
    {
        if (WCPOST()->setting_collection->isEnabled(WCPOST_Settings::SETTING_ORDER_STATUS_AUTOMATION)) {
            $order->update_status(
                $this->getSetting(WCPOST_Settings::SETTING_AUTOMATIC_ORDER_STATUS),
                __("PostNL shipment created:", "woocommerce-postnl")
            );
        }
    }

    /**
     * @param array $order_ids
     * @param array $shipment_ids
     * @param int   $offset
     *
     * @return array
     * @throws Exception
     */
    private function printLabels(array $order_ids, array $shipment_ids, int $offset)
    {
        if (! empty($shipment_ids)) {
            $return = $this->downloadOrGetUrlOfLabels(
                $shipment_ids,
                $order_ids,
                $offset
            );
        } else {
            $order_ids = $this->filterOrderDestinations($order_ids);
            $return    = $this->getOrderLabels($order_ids, $offset);
        }

        return $return;
    }

    /**
     * @param $order_ids
     * @param $offset
     * @param $print
     *
     * @return array|void
     * @throws ApiException
     * @throws ErrorException
     * @throws MissingFieldException
     * @throws Exception
     */
    private function addShipments($order_ids, $offset, $print)
    {
        $order_ids = $this->filterOrderDestinations($order_ids);

        if (empty($order_ids)) {
            $this->errors[] =
                __(
                    "The order(s) you have selected have invalid shipping countries.",
                    "woocommerce-postnl"
                );

            return;
        }

        // if we're going to print directly, we need to process the orders first, regardless of the settings
        $process = $print === "yes";
        $return  = $this->add_shipments($order_ids, $process);

        // When adding shipments, store $return for use in admin_notice
        // This way we can refresh the page (JS) to show all new buttons
        if ($print === "no" || $print === "after_reload") {
            update_option("wcpostnl_admin_notices", $return);
            if ($print === "after_reload") {
                $print_queue = [
                    "order_ids"    => $order_ids,
                    "shipment_ids" => $return["success_ids"],
                    "offset"       => isset($offset) && is_numeric($offset) ? $offset % 4 : 0,
                ];
                update_option("wcpostnl_print_queue", $print_queue);
            }
        }

        return $return;
    }

    /**
     * Save created track & trace information as meta data to the corresponding order(s).
     *
     * @param MyParcelCollection $collection
     * @param array              $order_ids
     */
    public static function saveTrackTracesToOrders(MyParcelCollection $collection, array $order_ids): void
    {
        foreach ($order_ids as $order_id) {
            $trackTraces = [];

            foreach ($collection->getConsignmentsByReferenceId($order_id) as $consignment) {
                /**
                 * @var AbstractConsignment $consignment
                 */
                array_push($trackTraces, $consignment->getBarcode());
            }

            WCPN_Export::addTrackTraceNoteToOrder($order_id, $trackTraces);
        }
    }
}

return new WCPN_Export();

```
