Skip to content
Snippets Groups Projects
Select Git revision
  • d7a4bf734105891a05731bc5a06049645c6b63e3
  • main default protected
  • trading_hours
  • refactor_trading_hours
  • audit_cleaning_cater_for_non_struct_fields
  • remove-info-logs
  • sl-refactor
  • 18-use-scan-for-param-values
  • 17-order-search-results
  • 4-simplify-framework-2
  • 1-http-error
  • v1.297.0
  • v1.296.0
  • v1.295.0
  • v1.294.0
  • v1.293.0
  • v1.292.0
  • v1.291.0
  • v1.290.0
  • v1.289.0
  • v1.288.0
  • v1.287.0
  • v1.286.0
  • v1.285.0
  • v1.284.0
  • v1.283.0
  • v1.282.0
  • v1.281.0
  • v1.280.0
  • v1.279.0
  • v1.278.0
31 results

error.go

Blame
  • BobGo.php 37.05 KiB
    <?php
    declare(strict_types=1);
    
    namespace BobGroup\BobGo\Model\Carrier;
    
    use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
    use Magento\CatalogInventory\Api\StockRegistryInterface;
    use Magento\Checkout\Api\Data\ShippingInformationInterface;
    use Magento\Directory\Helper\Data;
    use Magento\Directory\Model\CountryFactory;
    use Magento\Directory\Model\CurrencyFactory;
    use Magento\Directory\Model\RegionFactory;
    use Magento\Framework\App\Config\ScopeConfigInterface;
    use Magento\Framework\Controller\Result\JsonFactory;
    use Magento\Framework\DataObject;
    use Magento\Framework\Exception\LocalizedException;
    use Magento\Framework\HTTP\Client\CurlFactory;
    use Magento\Framework\Module\Dir\Reader;
    use Magento\Framework\Xml\Security;
    use Magento\Quote\Model\Quote\Address\RateRequest;
    use Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory;
    use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;
    use Magento\Sales\Model\Order\Shipment;
    use Magento\Shipping\Model\Carrier\AbstractCarrier;
    use Magento\Shipping\Model\Carrier\AbstractCarrierOnline;
    use Magento\Shipping\Model\Rate\Result;
    use Magento\Shipping\Model\Rate\ResultFactory;
    use Magento\Shipping\Model\Simplexml\ElementFactory;
    use Magento\Shipping\Model\Tracking\Result\StatusFactory;
    use Magento\Store\Model\ScopeInterface;
    use Magento\Store\Model\StoreManagerInterface;
    use Psr\Log\LoggerInterface;
    use Magento\Framework\App\RequestInterface;
    use Magento\Framework\App\Request\Http as MagentoHttp;
    
    /**
     * Bob Go shipping implementation
     * @website    https://www.bobgo.co.za
     * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
     * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
     * @SuppressWarnings(PHPMD.TooManyFields)
     */
    class BobGo extends AbstractCarrierOnline implements \Magento\Shipping\Model\Carrier\CarrierInterface
    {
        /**
         * Code of the carrier
         * @var string
         */
        public const CODE = 'bobgo';
    
        /**
         * Units constant
         * @var int
         */
        public const UNITS = 100;
    
        /**
         * Code of the carrier
         *
         * @var string
         */
        protected $_code = self::CODE;
    
        /**
         * Rate request data
         *
         * @var RateRequest|null
         */
        protected $_request = null;
    
        /**
         * Rate result data
         *
         * @var Result|null
         */
        protected $_result = null;
    
        /**
         * Container types that could be customized for bobgo carrier
         *
         * @var string[]
         */
        protected $_customizableContainerTypes = ['YOUR_PACKAGING'];
    
        /**
         * @var StoreManagerInterface
         */
        protected StoreManagerInterface $_storeManager;
    
        /**
         * @var CollectionFactory
         */
        protected CollectionFactory $_productCollectionFactory;
    
        /**
         * @var DataObject
         */
        private DataObject $_rawTrackingRequest;
    
        /**
         * @var \Magento\Framework\HTTP\Client\Curl
         */
        protected \Magento\Framework\HTTP\Client\Curl $curl;
    
        /**
         * @var ScopeConfigInterface
         */
        protected ScopeConfigInterface $scopeConfig;
    
        /**
         * @var JsonFactory
         */
        protected JsonFactory $jsonFactory;
    
        /**
         * @var AdditionalInfo
         */
        public AdditionalInfo $additionalInfo;
    
        /**
         * @var MagentoHttp
         */
        protected MagentoHttp $request;
    
        /**
         * BobGo constructor.
         *
         * @param ScopeConfigInterface $scopeConfig
         * @param ErrorFactory $rateErrorFactory
         * @param LoggerInterface $logger
         * @param Security $xmlSecurity
         * @param ElementFactory $xmlElFactory
         * @param ResultFactory $rateFactory
         * @param MethodFactory $rateMethodFactory
         * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory
         * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory
         * @param StatusFactory $trackStatusFactory
         * @param RegionFactory $regionFactory
         * @param CountryFactory $countryFactory
         * @param CurrencyFactory $currencyFactory
         * @param Data $directoryData
         * @param StockRegistryInterface $stockRegistry
         * @param StoreManagerInterface $storeManager
         * @param CollectionFactory $productCollectionFactory
         * @param JsonFactory $jsonFactory
         * @param CurlFactory $curlFactory
         * @param MagentoHttp $request
         * @param array<string,mixed> $data
         */
        public function __construct(
            ScopeConfigInterface $scopeConfig,
            ErrorFactory $rateErrorFactory,
            LoggerInterface $logger,
            Security $xmlSecurity,
            ElementFactory $xmlElFactory,
            ResultFactory $rateFactory,
            MethodFactory $rateMethodFactory,
            \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory,
            \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory,
            StatusFactory $trackStatusFactory,
            RegionFactory $regionFactory,
            CountryFactory $countryFactory,
            CurrencyFactory $currencyFactory,
            Data $directoryData,
            StockRegistryInterface $stockRegistry,
            StoreManagerInterface $storeManager,
            CollectionFactory $productCollectionFactory,
            JsonFactory $jsonFactory,
            CurlFactory $curlFactory,
            MagentoHttp $request,
            array $data = []
        ) {
            $this->request = $request;
            $this->_storeManager = $storeManager;
            $this->_productCollectionFactory = $productCollectionFactory;
            $this->scopeConfig = $scopeConfig;
    
            parent::__construct(
                $scopeConfig,
                $rateErrorFactory,
                $logger,
                $xmlSecurity,
                $xmlElFactory,
                $rateFactory,
                $rateMethodFactory,
                $trackFactory,
                $trackErrorFactory,
                $trackStatusFactory,
                $regionFactory,
                $countryFactory,
                $currencyFactory,
                $directoryData,
                $stockRegistry,
                $data
            );
    
            $this->jsonFactory = $jsonFactory;
            $this->curl = $curlFactory->create();
            $this->additionalInfo = new AdditionalInfo($countryFactory, $this->request);
        }
    
        /**
         * Gets the base URL of the store by stripping the http:// or https:// and www. from the URL.
         *
         * @return string
         */
        public function getBaseUrl(): string
        {
            /** @var Store $store */
            $store = $this->_storeManager->getStore();
            $storeBase = $store->getBaseUrl();
    
            // Remove protocol (http:// or https://)
            $host = preg_replace('#^https?://#', '', $storeBase);
    
            // Ensure $host is a string before using it in explode
            $host = $host ?? '';
    
            // Remove everything after the host (e.g., paths, query strings)
            $host = explode('/', $host)[0];
    
            // If the host starts with 'www.', remove it
            if (strpos($host, 'www.') === 0) {
                $host = substr($host, 4);
            }
    
            return $host;
        }
    
        /**
         * Makes a request to the Bob Go API to get shipping rates for the cart.
         *
         * @param array<string,mixed> $payload
         * @return array<int|string, mixed>
         */
        public function getRates(array $payload): array
        {
            $rates = $this->uRates($payload);
    
            // Ensure the return value is always an array, even if uRates returns null
            return $rates ?? [];
        }
    
        /**
         * Processing additional validation to check if the carrier is applicable.
         *
         * @param DataObject $request
         * @return $this|bool|\Magento\Framework\DataObject
         */
        public function processAdditionalValidation(DataObject $request)
        {
            /** @var RateRequest $rateRequest */
            $rateRequest = $request;
    
            if (!count($this->getAllItems($rateRequest))) {
                return false;
            }
    
            $maxAllowedWeight = 500;
            $errorMsg = '';
            $configErrorMsg = $this->getConfigData('specificerrmsg');
            $defaultErrorMsg = __('The shipping module is not available.');
            $showMethod = $this->getConfigData('showmethod');
    
            /** @var Item $item */
            foreach ($this->getAllItems($rateRequest) as $item) {
                $product = $item->getProduct();
                if ($product && $product->getId()) {
                    $weight = $product->getWeight();
                    $websiteId = (int) $item->getStore()->getWebsiteId(); // Ensure $websiteId is an integer
                    $stockItemData = $this->stockRegistry->getStockItem($product->getId(), $websiteId);
                    $doValidation = true;
    
                    if ($stockItemData->getIsQtyDecimal() && $stockItemData->getIsDecimalDivided()) {
                        if ($stockItemData->getEnableQtyIncrements() && $stockItemData->getQtyIncrements()) {
                            $weight = $weight * $stockItemData->getQtyIncrements();
                        } else {
                            $doValidation = false;
                        }
                    } elseif ($stockItemData->getIsQtyDecimal() && !$stockItemData->getIsDecimalDivided()) {
                        $weight = $weight * $item->getQty();
                    }
    
                    if ($doValidation && $weight > $maxAllowedWeight) {
                        $errorMsg = $configErrorMsg ? $configErrorMsg : $defaultErrorMsg;
                        break;
                    }
                }
            }
    
            if (!$errorMsg && !$rateRequest->getDestPostcode()
                && $this->isZipCodeRequired($rateRequest->getDestCountryId())) {
                $errorMsg = __('This shipping method is not available. Please specify the zip code.');
            }
    
            if ($rateRequest->getDestCountryId() == 'ZA') {
                $errorMsg = '';
            } else {
                $errorMsg = $configErrorMsg ? $configErrorMsg : $defaultErrorMsg;
            }
    
            if ($errorMsg && $showMethod) {
                $error = $this->_rateErrorFactory->create();
                $error->setCarrier($this->_code);
                $error->setCarrierTitle($this->getConfigData('title'));
                $error->setErrorMessage($errorMsg);
    
                return $error;
            } elseif ($errorMsg) {
                return false;
            }
    
            return $this;
        }
    
        /**
         * Collect and get rates for this shipping method based on information in $request.
         *
         * This is a default function that is called by Magento to get the shipping rates for the cart.
         *
         * @param RateRequest $request
         * @return Result|bool|null
         */
        public function collectRates(RateRequest $request)
        {
            // Make sure that Shipping method is enabled
            if (!$this->isActive()) {
                return false;
            }
    
            /**
             * Gets the destination company name from Company Name field in the checkout page.
             * This method is used as the last resort to get the company name since the company name is
             * not available in _rateFactory.
             */
            $destComp = $this->getDestComp();
            $destSuburb = $this->getDestSuburb();
    
            /** @var \Magento\Shipping\Model\Rate\Result $result */
            $result = $this->_rateFactory->create();
    
            $destination = $request->getDestPostcode();
            $destCountry = $request->getDestCountryId();
            $destRegion = $request->getDestRegionCode();
            $destCity = $request->getDestCity();
            $destStreet = $request->getDestStreet();
    
            /** Destination Information */
            [$destStreet1, $destStreet2, $destStreet3] = $this->destStreet($destStreet);
    
            /** Origin Information */
            [
                $originStreet,
                $originRegion,
                $originCountry,
                $originCity,
                $originStreet1,
                $originStreet2,
                $storeName,
                $baseIdentifier,
                $originSuburb,
                $weightUnit
            ] = $this->storeInformation();
    
            // Ensure weightUnit is always a string
            $weightUnit = $weightUnit ?? '';
    
            /** Get all items in cart */
            $items = $request->getAllItems();
            $itemsArray = [];
            $itemsArray = $this->getStoreItems($items, $weightUnit, $itemsArray);
    
            $payload = [
                'identifier' => $baseIdentifier,
                'rate' => [
                    'origin' => [
                        'company' => $storeName,
                        'address1' => $originStreet1,
                        'address2' => $originStreet2,
                        'city' => $originCity,
                        'suburb' => $originSuburb,
                        'province' => $originRegion,
                        'country_code' => $originCountry,
                        'postal_code' => $originStreet,
                    ],
                    'destination' => [
                        'company' => $destComp,
                        'address1' => $destStreet1,
                        'address2' => $destStreet2,
                        'suburb' => $destSuburb,
                        'city' => $destCity,
                        'province' => $destRegion,
                        'country_code' => $destCountry,
                        'postal_code' => $destination,
                    ],
                    'items' => $itemsArray,
                ]
            ];
    
            $this->_getRates($payload, $result);
    
            return $result;
        }
    
        /**
         * Retrieves store information including origin details.
         *
         * @return array<int, string|null>
         */
        public function storeInformation(): array
        {
            /** Store Origin details */
            $originCountry = $this->getStringValue('general/store_information/country_id');
            $originRegion = $this->getStringValue('general/store_information/region_id');
            $originCity = $this->getStringValue('general/store_information/city');
            $originStreet = $this->getStringValue('general/store_information/postcode');
            $originStreet1 = $this->getStringValue('general/store_information/street_line1');
            $originStreet2 = $this->getStringValue('general/store_information/street_line2');
            $storeName = $this->getStringValue('general/store_information/name');
            $originSuburb = $this->getStringValue('general/store_information/suburb');
            $weightUnit = $this->getStringValue('general/locale/weight_unit');
            $baseIdentifier = $this->getBaseUrl();
    
            return [
                $originStreet,
                $originRegion,
                $originCountry,
                $originCity,
                $originStreet1,
                $originStreet2,
                $storeName,
                $baseIdentifier,
                $originSuburb,
                $weightUnit,
            ];
        }
    
        /**
         * Safely retrieve a configuration value as a string or null.
         *
         * @param string $path
         * @return string|null
         */
        protected function getStringValue(string $path): ?string
        {
            $value = $this->_scopeConfig->getValue($path, ScopeInterface::SCOPE_STORE);
            return is_scalar($value) ? (string) $value : null;
        }
    
        /**
         * Get result of request
         *
         * @return Result|null
         */
        public function getResult()
        {
            if (!$this->_result) {
                $this->_result = $this->_trackFactory->create();
            }
            return $this->_result;
        }
    
        /**
         * Get final price for shipping method with handling fee per package
         *
         * @param float $cost
         * @param string $handlingType
         * @param float $handlingFee
         * @return float
         */
        protected function _getPerpackagePrice($cost, $handlingType, $handlingFee)
        {
            if ($handlingType == AbstractCarrier::HANDLING_TYPE_PERCENT) {
                return $cost + $cost * $this->_numBoxes * $handlingFee / self::UNITS;
            }
    
            return $cost + $this->_numBoxes * $handlingFee;
        }
    
        /**
         * Get final price for shipping method with handling fee per order
         *
         * @param float $cost
         * @param string $handlingType
         * @param float $handlingFee
         * @return float
         */
        protected function _getPerorderPrice($cost, $handlingType, $handlingFee)
        {
            if ($handlingType == self::HANDLING_TYPE_PERCENT) {
                return $cost + $cost * $handlingFee / self::UNITS;
            }
    
            return $cost + $handlingFee;
        }
    
        /**
         * Get configuration data of carrier
         *
         * @param string $type
         * @param string $code
         * @return array<string, \Magento\Framework\Phrase>|string|false
         * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
         */
        public function getCode($type, $code = '')
        {
            $codes = [
                'method' => [
                    'bobGo' => __('BobGo'),
                ],
                'unit_of_measure' => [
                    'KGS' => __('Kilograms'),
                    'LBS' => __('Pounds'),
                ],
            ];
    
            if (!isset($codes[$type])) {
                return false;
            } elseif ('' === $code) {
                return $codes[$type];
            }
    
            if (!isset($codes[$type][$code])) {
                return false;
            } else {
                return (string) $codes[$type][$code]; // Convert \Magento\Framework\Phrase to string
            }
        }
    
        /**
         * Get allowed shipping methods
         *
         * @return array<string, mixed>
         */
        public function getAllowedMethods(): array
        {
            $allowedMethods = $this->getConfigData('allowed_methods');
            if ($allowedMethods === false) {
                return []; // Return an empty array if no allowed methods are configured
            }
    
            $allowed = explode(',', $allowedMethods);
            $arr = [];
            foreach ($allowed as $k) {
                $arr[$k] = $this->getCode('method', $k);
            }
    
            return $arr;
        }
    
        /**
         * Do shipment request to carrier web service, obtain Print Shipping Labels, and process errors in response.
         *
         * Also another magic function that is required to be implemented by the carrier model.
         *
         * @param \Magento\Framework\DataObject $request
         * @return \Magento\Framework\DataObject|null
         */
        protected function _doShipmentRequest(\Magento\Framework\DataObject $request)
        {
            return null;
        }
    
        /**
         * For multi-package shipments. Delete requested shipments if the current shipment request fails.
         *
         * @param mixed $data
         * @return bool
         */
        public function rollBack($data): bool
        {
            // Return false if $data is not an array
            if (!is_array($data)) {
                return false;
            }
    
            return true;
        }
    
        /**
         * Return container types of carrier.
         *
         * @param \Magento\Framework\DataObject|null $params
         * @return array<string, mixed>|false
         * @SuppressWarnings(PHPMD.CyclomaticComplexity)
         */
        public function getContainerTypes(\Magento\Framework\DataObject $params = null)
        {
            $result = [];
            $allowedContainers = $this->getConfigData('containers');
            if ($allowedContainers) {
                $allowedContainers = explode(',', $allowedContainers);
            }
            if ($allowedContainers) {
                foreach ($allowedContainers as $container) {
                    $result[$container] = $this->getCode('container_types', $container);
                }
            }
    
            return !empty($result) ? $result : false;
        }
    
        /**
         * Return delivery confirmation types of carrier.
         *
         * @param \Magento\Framework\DataObject|null $params
         * @return array<int|string, mixed>
         * @SuppressWarnings(PHPMD.UnusedFormalParameter)
         */
        public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $params = null): array
        {
            $types = $this->getCode('delivery_confirmation_types');
    
            // Ensure it returns an array, even if getCode returns false
            return is_array($types) ? $types : [];
        }
    
        /**
         * Recursive replace sensitive fields in debug data by the mask.
         *
         * @param mixed $data
         * @return mixed
         */
        protected function filterDebugData($data)
        {
            if (!is_array($data)) {
                return $data; // Return early if $data is not an array.
            }
    
            foreach (array_keys($data) as $key) {
                if (is_array($data[$key])) {
                    $data[$key] = $this->filterDebugData($data[$key]);
                } elseif (in_array($key, $this->_debugReplacePrivateDataKeys)) {
                    $data[$key] = self::DEBUG_KEYS_MASK;
                }
            }
    
            return $data;
        }
    
        /**
         * Format a date to 'd M Y'.
         *
         * @param string $date
         * @return string
         */
        public function formatDate(string $date): string
        {
            $timestamp = strtotime($date);
            if ($timestamp === false) {
                // Handle the error or return a default value, for example:
                return 'Invalid date';
            }
            return date('d M Y', $timestamp);
        }
    
        /**
         * Format a time to 'H:i'.
         *
         * @param string $time
         * @return string
         */
        public function formatTime(string $time): string
        {
            $timestamp = strtotime($time);
            if ($timestamp === false) {
                // Handle the error or return a default value, for example:
                return 'Invalid time';
            }
            return date('H:i', $timestamp);
        }
    
        /**
         * Get the API URL for Bob Go.
         *
         * @return string
         */
        private function getApiUrl(): string
        {
            return UData::RATES_ENDPOINT;
        }
    
        private function getWebhookUrl(): string
        {
            return UData::WEBHOOK_URL;
        }
    
        /**
         * Perform API Request to Bob Go API and return response.
         *
         * @param array<string,mixed> $payload The payload for the API request.
         * @param Result $result The result object to append the rates.
         * @return void
         */
        protected function _getRates(array $payload, Result $result): void
        {
            $rates = $this->uRates($payload);
    
            // Ensure $rates is an array before passing it to _formatRates
            if (is_array($rates)) {
                $this->_formatRates($rates, $result);
            } else {
                $this->_logger->error('Bob Go API returned an invalid response');
            }
        }
    
        /**
         * Format rates from Bob Go API response and append to rate result instance of carrier.
         *
         * @param array<int|string,mixed> $rates The rates data from the API.
         * @param Result $result The result object to append the rates.
         * @return void
         */
        protected function _formatRates(array $rates, Result $result): void
        {
            if (empty($rates['rates']) || !is_array($rates['rates'])) {  // Validate that 'rates' exists and is an array
                $error = $this->_rateErrorFactory->create();
                $error->setCarrierTitle($this->getConfigData('title'));
                $error->setErrorMessage($this->getConfigData('specificerrmsg'));
    
                $result->append($error);
                return;
            }
    
            foreach ($rates['rates'] as $rate) {
                if (!is_array($rate)) {
                    continue;  // Skip if the rate is not an array
                }
    
                $method = $this->_rateMethodFactory->create();
    
                // Set the carrier code
                $method->setCarrier(self::CODE);
    
                // Strip out the redundant 'bobgo_' prefix if present
                $serviceCode = $rate['service_code'] ?? '';
                if (is_string($serviceCode) && strpos($serviceCode, 'bobgo_') === 0) {
                    $serviceCode = substr($serviceCode, strlen('bobgo_'));
                }
    
                // Set the method with the modified service code
                $method->setMethod($serviceCode);
    
                // Set additional info if required
                if ($this->getConfigData('additional_info') == 1) {
                    $min_delivery_date = isset($rate['min_delivery_date']) && is_string($rate['min_delivery_date'])
                        ? $this->getWorkingDays(date('Y-m-d'), $rate['min_delivery_date'])
                        : null;
    
                    $max_delivery_date = isset($rate['max_delivery_date']) && is_string($rate['max_delivery_date'])
                        ? $this->getWorkingDays(date('Y-m-d'), $rate['max_delivery_date'])
                        : null;
    
                    $this->deliveryDays($min_delivery_date, $max_delivery_date, $method);
                }
    
                // Set the method title, price, and cost
                $service_name = $rate['service_name'] ?? '';
                if (!is_string($service_name)) {
                    $service_name = '';
                }
                $method->setMethodTitle($service_name);
    
                $price = $rate['total_price'] ?? 0;
                if (!is_numeric($price)) {
                    $price = 0;
                }
                $cost = $rate['total_price'] ?? 0;
                if (!is_numeric($cost)) {
                    $cost = 0;
                }
    
                $method->setPrice((float)$price);
                $method->setCost((float)$cost);
    
                $result->append($method);
            }
        }
    
        /**
         * Get Working Days between time of checkout and delivery date (min and max).
         *
         * @param string $startDate
         * @param string $endDate
         * @return int
         */
        public function getWorkingDays(string $startDate, string $endDate): int
        {
            $begin = strtotime($startDate);
            $end = strtotime($endDate);
    
            // Check if strtotime failed
            if ($begin === false || $end === false || $begin > $end) {
                return 0; // or throw an exception if preferred
            }
    
            $no_days = 0;
            $weekends = 0;
    
            while ($begin <= $end) {
                $no_days++; // number of days in the given interval
                $what_day = date("N", $begin);
                if ($what_day > 5) { // 6 and 7 are weekend days
                    $weekends++;
                }
                $begin += 86400; // +1 day
            }
    
            return $no_days - $weekends;
        }
    
        /**
         * Build the payload for Bob Go API request and return the response.
         *
         * @param array<string,mixed> $payload The payload for the API request.
         * @return array<int|string, mixed>|null The decoded response, or null if the response could not be decoded
         * or is not an array.
         */
        protected function uRates(array $payload): ?array
        {
            $this->curl->addHeader('Content-Type', 'application/json');
    
            $payloadJson = json_encode($payload);
            if ($payloadJson === false) {
                // Handle JSON encoding failure if necessary
                return null; // or throw an exception
            }
    
            $this->curl->post($this->getApiUrl(), $payloadJson);
            $rates = $this->curl->getBody();
    
            $rates = json_decode($rates, true);
    
            // Ensure that $rates is an array or return null
            if (is_array($rates)) {
                return $rates;
            }
    
            return null;
        }
    
        /**
         * Splits a destination street address into up to three lines if it contains newline characters.
         *
         * @param string $destStreet The full street address.
         * @return string[] An array containing up to three lines of the street address.
         */
        protected function destStreet(string $destStreet): array
        {
            if (strpos($destStreet, "\n") !== false) {
                $destStreet = explode("\n", $destStreet);
                $destStreet1 = $destStreet[0];
                $destStreet2 = $destStreet[1];
                $destStreet3 = $destStreet[2] ?? '';
            } else {
                $destStreet1 = $destStreet;
                $destStreet2 = '';
                $destStreet3 = '';
            }
            return [$destStreet1, $destStreet2, $destStreet3];
        }
    
        /**
         * Sets the carrier title with the estimated delivery days range based on minimum and maximum delivery dates.
         *
         * @param int|null $min_delivery_date Minimum estimated delivery date in days.
         * @param int|null $max_delivery_date Maximum estimated delivery date in days.
         * @param \Magento\Quote\Model\Quote\Address\RateResult\Method $method The shipping method instance
         * to set the carrier title.
         * @return void
         */
        protected function deliveryDays(
            ?int $min_delivery_date,
            ?int $max_delivery_date,
            \Magento\Quote\Model\Quote\Address\RateResult\Method $method
        ): void {
            if ($min_delivery_date === null || $max_delivery_date === null) {
                return;
            }
    
            if ($min_delivery_date !== $max_delivery_date) {
                $method->setCarrierTitle('Delivery in ' . $min_delivery_date . ' - ' . $max_delivery_date . ' days');
            } else {
                if ($min_delivery_date && $max_delivery_date == 1) {
                    $method->setCarrierTitle('Delivery in ' . $min_delivery_date . ' day');
                } else {
                    $method->setCarrierTitle('Delivery in ' . $min_delivery_date . ' days');
                }
            }
        }
    
        /**
         * Retrieves the destination company name from the additional information.
         *
         * @return mixed|string The destination company name.
         */
        public function getDestComp(): mixed
        {
            return $this->additionalInfo->getDestComp();
        }
    
        /**
         * Retrieves the destination suburb from the additional information.
         *
         * @return mixed|string The destination suburb.
         */
        public function getDestSuburb(): mixed
        {
            return $this->additionalInfo->getSuburb();
        }
    
        /**
         * Calculates the item weight in grams based on the provided weight unit.
         *
         * @param string $weightUnit The unit of weight, either 'KGS' or another unit (assumed to be pounds).
         * @param \Magento\Quote\Model\Quote\Item $item The item whose weight is to be calculated.
         * @return float The weight of the item in grams.
         */
        public function getItemWeight(string $weightUnit, \Magento\Quote\Model\Quote\Item $item): float
        {
            // 1 lb = 453.59237 g exact. 1 kg = 1000 g. 1 lb = 0.45359237 kg
            if ($weightUnit === 'KGS') {
                $mass = $item->getWeight() ? $item->getWeight() * 1000 : 0;
            } else {
                // Pound to Kilogram Conversion Formula
                $mass = $item->getWeight() ? $item->getWeight() * 0.45359237 * 1000 : 0;
            }
            return $mass;
        }
    
        /**
         * Processes the items in the cart, calculates their weights, and prepares an array of item details.
         *
         * @param \Magento\Quote\Model\Quote\Item[] $items The items in the cart.
         * @param string $weightUnit The unit of weight used for the items.
         * @param array<int,array<string,mixed>> $itemsArray The array to store the processed item details.
         * @return array<int, array<string, mixed>> The array containing details of each item,
         * including SKU, quantity, price, and weight.
         */
        public function getStoreItems(
            array $items,
            string $weightUnit,
            array $itemsArray
        ): array {
            foreach ($items as $item) {
                $mass = $this->getItemWeight($weightUnit, $item);
    
                $itemsArray[] = [
                    'sku' => $item->getSku(),
                    'quantity' => $item->getQty(),
                    'price' => $item->getPrice(),
                    'weight' => round($mass),
                ];
            }
    
            return $itemsArray;
        }
    
        /**
         * Trigger a test for rates.
         *
         * @return array<int|string, mixed>|bool Returns an array of results or false on failure.
         */
        public function triggerRatesTest(): array|bool
        {
            // Check if the 'Show rates for checkout' setting is enabled
            $isEnabled = $this->scopeConfig->getValue(
                'carriers/bobgo/active',
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            );
    
            if ($isEnabled) {
                // Sample test payload, replace with actual structure
                $payload = [
                    'identifier' => $this->getBaseUrl(),
                    'rate' => [
                        'origin' => [
                            'company' => 'Jamie Ds Emporium',
                            'address1' => '36 Marelu Street',
                            'address2' => 'Six Fountains Estate',
                            'city' => 'Pretoria',
                            'suburb' => 'Pretoria',
                            'province' => 'GT',
                            'country_code' => 'ZA',
                            'postal_code' => '0081',
                        ],
                        'destination' => [
                            'company' => 'Test Company',
                            'address1' => '456 Test Ave',
                            'address2' => '',
                            'suburb' => 'Test Suburb',
                            'city' => 'Test City',
                            'province' => 'Test Province',
                            'country_code' => 'ZA',
                            'postal_code' => '3000',
                        ],
                        'items' => [
                            [
                                'sku' => 'test-sku-1',
                                'quantity' => 1,
                                'price' => 100.00,
                                'weight' => 500, // in grams
                            ]
                        ],
                    ]
                ];
    
                try {
                    // Perform the API request
                    $payloadJson = json_encode($payload);
                    if ($payloadJson === false) {
                        throw new \RuntimeException('Failed to encode payload to JSON.');
                    }
    
                    $this->curl->addHeader('Content-Type', 'application/json');
                    $this->curl->post($this->getApiUrl(), $payloadJson);
                    $statusCode = $this->curl->getStatus();
                    $responseBody = $this->curl->getBody();
    
                    // Decode the response
                    $response = json_decode($responseBody, true);
    
                    if (!is_array($response)) {
                        throw new LocalizedException(__('Invalid response format.'));
                    }
    
                    // Check if the response contains a 'message' (indicating an error)
                    if (isset($response['message'])) {
                        throw new LocalizedException(__('Error from BobGo: %1', $response['message']));
                    }
    
                    // Check if the response contains rates with a valid id field
                    if (isset($response['rates']) && is_array($response['rates']) && !empty($response['rates'])) {
                        foreach ($response['rates'] as $rate) {
                            if (isset($rate['id']) && $rate['id'] !== null) {
                                return $response; // Successful response with a valid id
                            }
                        }
                        throw new LocalizedException(__('Rates received but id field is empty or invalid.'));
                    } else {
                        throw new LocalizedException(__('Received response but no valid rates were found.'));
                    }
                } catch (\Exception $e) {
                    return false;
                }
            }
            return false;
        }
    
        public function isWebhookEnabled(): bool
        {
            $enabled = $this->scopeConfig->getValue(
                'carriers/bobgo/enable_webhooks',
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            );
    
            // Cast the value to a boolean
            return filter_var($enabled, FILTER_VALIDATE_BOOLEAN);
        }
    
    
        public function triggerWebhookTest(): bool
        {
            $webhookKey = $this->scopeConfig->getValue(
                'carriers/bobgo/webhook_key',
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            );
    
            $isEnabled = $this->scopeConfig->getValue(
                'carriers/bobgo/enable_webhooks',
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            );
    
            // Convert the string to a boolean value
            $isEnabled = filter_var($isEnabled, FILTER_VALIDATE_BOOLEAN);
    
    //        if (!$webhookKey) {
    //            $this->_logger->error('Webhook key not configured.');
    //            return false;
    //        }
    
            $storeId = strval($this->_storeManager->getStore()->getId());
    
            $payload = [
                'event' => 'webhook_validation',
                'channel_identifier' => $this->getBaseUrl(),
                'store_id' => $storeId,
                'webhooks_enabled' => $isEnabled,
            ];
    
            try {
                // Generate the HMAC-SHA256 hash as raw binary data
                $rawSignature = hash_hmac('sha256', $storeId, $webhookKey, true);
                // Encode the binary data in Base64
                $signature = base64_encode($rawSignature);
                // Set headers and post the data
                $this->curl->addHeader('Content-Type', 'application/json');
                $this->curl->addHeader('X-M-Webhook-Signature', $signature);
    
                $payloadJson = json_encode($payload);
                $this->_logger->info('Webhooks payload: ' . $payloadJson);
                if ($payloadJson === false) {
                    throw new \RuntimeException('Failed to encode payload to JSON.');
                }
    
                $this->curl->addHeader('Content-Type', 'application/json');
                $this->curl->post($this->getWebhookUrl(), $payloadJson);
                $statusCode = $this->curl->getStatus();
                $this->_logger->info('Webhooks statuscode: ' . $statusCode);
                $responseBody = $this->curl->getBody();
                $this->_logger->info('Webhooks response: ' . $responseBody);
    
                $response = json_decode($responseBody, true);
    
                if ($statusCode == 200 && isset($response['success']) && $response['success'] === true) {
                    $this->_logger->info('Webhook validation successful.');
    //                throw new LocalizedException(__('Rates received but id field is empty or invalid.'));
                    return true;
                } else {
                    $this->_logger->error('Webhook validation failed: ' . ($response['message'] ?? 'Unknown error'));
    //                throw new LocalizedException(__('Rates received but id field is empty or invalid.'));
                    return false;
                }
            } catch (\Exception $e) {
                $this->_logger->error('Webhook validation exception: ' . $e->getMessage());
    //            throw new LocalizedException(__('Rates received but id field is empty or invalid.'));
                return false;
            }
        }
    
    }