postnl / postnl-magento2

This is the official Magento 2 extension for the logistics company PostNL. Add shipping options and parcelshops to your checkout. Create labels with track and trace functionality from the backend.
59 stars 60 forks source link

[BUG] Matrix rates: 'The country isn't available' exception because an incorrect scope is used #391

Open YachYaroslav opened 3 months ago

YachYaroslav commented 3 months ago

To Reproduce Steps to reproduce the behavior:

  1. Prerequisites
    1. Having at least 2 websites (scopes) configured in Magento
    2. Each website has its own list of available countries. (E.g. site1 has only the Netherlands and Belgium, while site2 has only Germany).
    3. One of the websites domain is being used for accessing and using the admin area (e.g. site1).
  2. Click on 'POSTNL' menu item in the admin navigation on the left.
  3. Click on 'Matrix Rates'.
  4. Add a new matrixrate (country: Germany, other values don't matter) for site2 (available countries in the configuration for site2 = [Germany]).
  5. Visit 'Matrix Rates' again to overview the grid with the rates.

Expected result We see the list with the matrix rates created in our Magento installation.

Actual result We see the message 'The country isn't available', which is an exception thrown when calling (in Ui/Component/Matrix/Listing/Columns/Country.php):

$countryInfo       = $this->countryInformationAcquirer->getCountryInfo($value);

Workaround Enable the country for site1, because in getCountryInfo the website configuration of current admin store view (so website site1 and not site2) is used.

Or (rewrite the country name retrieval a bit differently). My own class example (<column name="website_id" class="BytesGuru\PostNL\Ui\Component\Matrix\Listing\Columns\Website"/>):

<?php
declare(strict_types=1);

namespace BytesGuru\PostNL\Ui\Component\Matrix\Listing\Columns;

use Magento\Backend\Model\Auth\Session as AdminSession;
use Magento\Directory\Model\AllowedCountries;
use Magento\Directory\Model\Country as DirectoryCountry;
use Magento\Directory\Model\ResourceModel\Country\Collection as CountryCollection;
use Magento\Directory\Model\ResourceModel\Country\CollectionFactory as CountryCollectionFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Store\Model\ScopeInterface;
use Magento\Ui\Component\Listing\Columns\Column;

class Country extends Column
{
    private CountryCollectionFactory $countryCollectionFactory;

    /**
     * @var array<int, CountryCollection<DirectoryCountry>>
     */
    private array $countryCollectionPerWebsite;

    private AllowedCountries $allowedCountries;

    private AdminSession $adminSession;

    /**
     * @param ContextInterface $context
     * @param UiComponentFactory $uiComponentFactory
     * @param CountryCollectionFactory $countryCollectionFactory
     * @param AllowedCountries $allowedCountries
     * @param AdminSession $adminSession
     * @param array<string, mixed> $components
     * @param array<string, mixed> $data
     */
    public function __construct(
        ContextInterface         $context,
        UiComponentFactory       $uiComponentFactory,
        CountryCollectionFactory $countryCollectionFactory,
        AllowedCountries         $allowedCountries,
        AdminSession             $adminSession,
        array $components = [],
        array $data       = []
    ) {
        parent::__construct($context, $uiComponentFactory, $components, $data);
        $this->countryCollectionFactory = $countryCollectionFactory;
        $this->allowedCountries         = $allowedCountries;
        $this->adminSession             = $adminSession;
    }

    /**
     * Prepare Data Source
     *
     * @see Website::prepareDataSource()
     *
     * @param array<string, mixed> $dataSource
     *
     * @return array<string, mixed>
     */
    public function prepareDataSource(array $dataSource): array
    {
        if (isset($dataSource['data']['items'])) {
            $fieldName = $this->getData('name');

            foreach ($dataSource['data']['items'] as &$item) {
                $itemList        = explode(',', $item[$fieldName]);
                $countryNameList = [];

                foreach ($itemList as $value) {
                    try {
                        // _website_id is set in \BytesGuru\PostNL\Ui\Component\Matrix\Listing\Columns\Website
                        $countryNameList[] = $this->getFullCountryName($value, $item['_website_id']);
                    } catch (NoSuchEntityException $e) {
                        $countryNameList[] = __(
                            "Country by code '%1' is not enabled for the website '%2'.",
                            $value,
                            $item['_website_id']
                        );
                    }
                }

                $item[$fieldName] = implode(', ', $countryNameList);
            }
        }

        return $dataSource;
    }

    /**
     * Get the full name of a country by its ID.
     *
     * @param string $countryId The ID of the country.
     * @param int|string $websiteId The ID of the website.
     *
     * @return string The full name of the country (based on the current admin user locale).
     *
     * @throws NoSuchEntityException If the country is not available.
     */
    private function getFullCountryName(string $countryId, $websiteId): string
    {
        $websiteId = (int) $websiteId;

        if (isset($this->countryCollectionPerWebsite[$websiteId])) {
            $countryCollection = $this->countryCollectionPerWebsite[$websiteId];
        } else {
            $this->countryCollectionPerWebsite[$websiteId] = $this->countryCollectionFactory->create();
            $countryCollection                             = $this->countryCollectionPerWebsite[$websiteId];
            $allowedCountries                              = $this->allowedCountries->getAllowedCountries(
                ScopeInterface::SCOPE_WEBSITE,
                (string) $websiteId
            );
            if ($allowedCountries) {
                $countryCollection->addFieldToFilter('country_id', ['in' => $allowedCountries])->load();
            }
        }

        /** @var \Magento\Directory\Model\Country|null $country */
        $country = $countryCollection->getItemById($countryId);
        if (!$country) {
            throw new NoSuchEntityException(__("The country isn't available."));
        }

        $currentUser        = $this->adminSession->getUser();
        $currentAdminLocale = $currentUser ? $currentUser->getInterfaceLocale() : 'en_US';

        return $country->getName($currentAdminLocale);
    }
}

Note: we don't have the website_id available on the moment of \TIG\PostNL\Ui\Component\Matrix\Listing\Columns\Country::prepareDataSource execution, because it has been overwritten by its name for each row in \TIG\PostNL\Ui\Component\Matrix\Listing\Columns\Website::prepareDataSource. So the altering is required there as well or another method should be used for getting the full country name for each row.

Screenshots matrix-rates-exception