Magento2: QuoteValidator retrieving the wrong Allowed Countries in multi-store setup

Created on 21 Mar 2018  路  10Comments  路  Source: magento/magento2

Summary

This is an issue that happens in multi-store setups. If you have restricted countries in your websites (for example, a USA Website with US as only allowed country, and a EU website with EU-only allowed countries), Magento won't let you enter new orders in the admin or reorder, when trying to do that for non-default websites.

The following error will be shown:

_"Some addresses cannot be used due to country-specific configurations."_

You might also get the following error if you try to create a new customer:

_"Invalid value of "XXX" provided for the countryId field."_ (where XXX is the order's country code)

More details below.

Preconditions

I'm using Magento v2.2.3, with PHP 7.1, MySQL 5.7, Apache 2.4 on an Ubuntu 17.10.

Magento is setup from a default installation, and then the following was added:

  1. Two websites: US Website and EU website, along with corresponding stores and store views.

  2. A test product that is available in all websites, price is $0.

  3. _Set the US Website as the default one._

  4. In Stores -> Configuration -> General -> General -> Country Options -> Allow Countries, configure the following:

  • Default Config: Allow ALL countries.
  • US Website: Allow only the US.
  • EU Website: Allow only EU countries (or just enable France).
  1. The patch described in #11511 is in, though I think this is not strictly necessary to reproduce the general behavior.

Steps to reproduce

After the setup mentioned above is complete, try to do any of the following:

_Option A_

  1. Make a new order in the front-end using the EU store (which is restricted, in our case, to just France)
  2. Go to the admin and try to re-order that product.

_Option B_

  1. Go to the admin and try to directly create a new order a product for the EU store (again, with countries restricted to France).

_Option C_

  1. Go to the admin and try to create a new customer for the EU store (again, with countries restricted to France).

Expected result

The order or reorder should go through.

Actual result

The order won't go through, with one of the error messages described above.

Findings

For re-orders, I tracked it down to the following. In vendor/magento/module-quote/Model/QuoteValidator.php, you have this code:

// Checks if country id present in the allowed countries list.
if (!in_array(
    $quote->getShippingAddress()->getCountryId(),
    $this->allowedCountryReader->getAllowedCountries()
)) {
    throw new \Magento\Framework\Exception\LocalizedException(
        __('Some addresses cannot be used due to country-specific configurations.')
    );
}

Notice that the getAllowedCountries call is made using defaults. But that means this is what will get called in turn, in vendor/magento/module-directory/Model/AllowedCountries.php:

if (empty($scopeCode)) {
    $scopeCode = $this->getDefaultScopeCode($scope);
}

... and then the default store's scope code will be used (in our case, the US Store!). Since that store has a restricted allowed country setting (US), this will prevent the order in the non-default store (FR) from going through.

A fix for that behavior is to use the quote's store ID when calling getAllowedCountries:

if(!in_array(
    $quote->getShippingAddress()->getCountryId(),
    $this->allowedCountryReader->getAllowedCountries(
      \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $quote->getStoreId())
)) {
    throw new \Magento\Framework\Exception\LocalizedException(
      __('Some addresses cannot be used due to country-specific configurations.')
    );
}

Note that this fix might require the patch described in #11511 to be applied. That is a similar but separate situation, described here, that breaks the configuration fallback mechanism when resolving the store scope.

I haven't looked into the fix for the other error message (_"Invalid value of XXX provided in the countryId field"_ when trying to create a new customer), but it might be related to this. Probably a general review (and test cases) of the multi-site behavior on this area might be required.

Clear Description Confirmed Format is valid Ready for Work Reproduced on 2.2.x Reproduced on 2.3.x

All 10 comments

@leoquijano, thank you for your report.
We've acknowledged the issue and added to our backlog.

Thanks!

There are additional problems caused by this in the admin. Using the same setup as described above, you can enter a new order for France in the EU store, from the front-end. Now try to edit it in the admin. You will see that only the US is available as a country, even though the order was saved using France!

I tracked this down to file vendor/magento/module-sales/Block/Adminhtml/Order/Create/Form/Address.php contains block code for the admin area address editor.

  1. First, method _prepareForm fills the form with important information. This section is key:

    protected function _prepareForm()
    {
        // ...
    
        if ($this->_form->getElement('country_id')->getValue()) {
            $countryId = $this->_form->getElement('country_id')->getValue();
            $this->_form->getElement('country_id')->setValue(null);
            foreach ($this->_form->getElement('country_id')->getValues() as $country) {
                if ($country['value'] == $countryId) {
                    $this->_form->getElement('country_id')->setValue($countryId);
                }
            }
        }
    
        // ...
    
        return $this;
    }
    

    Basically this will retrieve $this->_form->getElement('country_id'). Since that's a "form object", it's like the backend for a HTML SELECT field, so it contains both the currently selected country, and also the list of all possible countries.

    Note that it iterates over the possible values, which are set somewhere else. If the currently selected value is not a valid one, it will remove it.

  2. Second, the processCountryOptions will fill that field with the list of countries. This is the implementation:

    private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement)
    {
        $storeId = $this->getBackendQuoteSession()->getStoreId();
        $options = $this->getCountriesCollection()
            ->loadByStore($storeId)
            ->toOptionArray();
    
        $countryElement->setValues($options);
    }
    

    Note that it retrieves the storeId from the current session. If you're creating a new order in the admin, this store ID is set by you directly at the beginning of that process. But when you're editing an existing order, it seems that the store is not set in the session (that getStoreId call returns null or empty). So when it tries to load the countries collection, it will go to the default store, which is the US, as described in my original post.

I don't know why processCountryOptions is called after filtering the currently selected country, and I don't know how the BackendQuoteSession object should be adjusted to properly select the store when editing orders.

I do know that as a workaround, if I comment out the code presented in item 1 above, and I comment out the loadByStore call in item 2 (to leave it out of the filter), the admin allows you to at least have all countries available (and the right one selected by default). That helps prevent Customer Support staff from accidentally corrupting existing non-US orders when trying to edit their addresses.

The same problem when trying to change the country in _backend > customer > address_

same problem with magento v2.2.4.
is there any update ?

same problem with magento v2.2.5.
This problem is common to https://github.com/magento/magento2/issues/12509
My solution:
1) declare plugin
app/code/[Namespace]/[Module]/etc/adminhtml/di.xml

 <type name="Magento\Sales\Controller\Adminhtml\Order\Create\Save">
        <plugin  name="fix_sales_save_new_order_multi_store" sortOrder="10" type="[Namespace]\[Module]\Plugin\Magento\Sales\Controller\Adminhtml\Order\Create\SavePlugin"/>
    </type>

2) create plugin class app/code/[Namespace]/[Module]/Magento/Sales/Plugin/Controller/Adminhtml/Order/Create/SavePlugin.php

<?php

namespace [Namespace]\[Module]\Plugin\Magento\Sales\Controller\Adminhtml\Order\Create;

class SavePlugin
{
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $storeManager;

    /**
     * @var \Magento\Backend\Model\Session\Quote
     */
    protected $quoteSession;

    /**
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Backend\Model\Session\Quote $quoteSession
     */
    public function __construct(
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Backend\Model\Session\Quote $quoteSession
    ) {
        $this->storeManager=$storeManager;
        $this->quoteSession = $quoteSession;
    }

    /**
     * @param \Magento\Sales\Controller\Adminhtml\Order\Create\Save $subject
     */
    public function beforeExecute(
        \Magento\Sales\Controller\Adminhtml\Order\Create\Save $subject
    ) {
        $this->storeManager->setCurrentStore($this->quoteSession->getStore());
        return [];
    }
}

same problem with magento v2.2.7 and 2.3.0
is there any update ?

Hi all.

The issue if fixed in 2.2-develop branch (not sure about the branch 2.3-develop).
For now for Magento 2.2.x you can apply patch from the commit https://github.com/magento/magento2/commit/d7d13a4cc8c8dfd21d2dc051e87676cab7c491be

We've tested it on 2.2.7 - the is fixed.

Hi @engcom-backlog-nazar. Thank you for working on this issue.
Looks like this issue is already verified and confirmed. But if you want to validate it one more time, please, go though the following instruction:

  • [ ] 1. Add/Edit Component: XXXXX label(s) to the ticket, indicating the components it may be related to.
  • [ ] 2. Verify that the issue is reproducible on 2.3-develop branch

    Details- Add the comment @magento-engcom-team give me 2.3-develop instance to deploy test instance on Magento infrastructure.
    - If the issue is reproducible on 2.3-develop branch, please, add the label Reproduced on 2.3.x.
    - If the issue is not reproducible, add your comment that issue is not reproducible and close the issue and _stop verification process here_!

  • [ ] 3. Verify that the issue is reproducible on 2.2-develop branch.

    Details- Add the comment @magento-engcom-team give me 2.2-develop instance to deploy test instance on Magento infrastructure.
    - If the issue is reproducible on 2.2-develop branch, please add the label Reproduced on 2.2.x

  • [ ] 4. If the issue is not relevant or is not reproducible any more, feel free to close it.

Hi @leoquijano thank you for you report, this issue has already fixed by this commit -> https://github.com/magento/magento2/commit/e22304a11d19cacc43a9df689eff9ce4f16a3c89 in 2.3-develop branch.

Thanks!

There are additional problems caused by this in the admin. Using the same setup as described above, you can enter a new order for France in the EU store, from the front-end. Now try to edit it in the admin. You will see that only the US is available as a country, even though the order was saved using France!

I tracked this down to file vendor/magento/module-sales/Block/Adminhtml/Order/Create/Form/Address.php contains block code for the admin area address editor.

1. First, method `_prepareForm` fills the form with important information. This section is key:
   ```
   protected function _prepareForm()
   {
       // ...

       if ($this->_form->getElement('country_id')->getValue()) {
           $countryId = $this->_form->getElement('country_id')->getValue();
           $this->_form->getElement('country_id')->setValue(null);
           foreach ($this->_form->getElement('country_id')->getValues() as $country) {
               if ($country['value'] == $countryId) {
                   $this->_form->getElement('country_id')->setValue($countryId);
               }
           }
       }

       // ...

       return $this;
   }
   ```


   Basically this will retrieve `$this->_form->getElement('country_id')`. Since that's a "form object", it's like the backend for a HTML SELECT field, so it contains both the currently selected country, and also the list of all possible countries.
   Note that it iterates over the possible values, which are set somewhere else. If the currently selected value is not a valid one, it will remove it.

2. Second, the `processCountryOptions` will fill that field with the list of countries. This is the implementation:
   ```
   private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement)
   {
       $storeId = $this->getBackendQuoteSession()->getStoreId();
       $options = $this->getCountriesCollection()
           ->loadByStore($storeId)
           ->toOptionArray();

       $countryElement->setValues($options);
   }
   ```


   Note that it retrieves the `storeId` from the current session. If you're creating a new order in the admin, this store ID is set by you directly at the beginning of that process. But when you're editing an existing order, it seems that the store is not set in the session (that `getStoreId` call returns `null` or empty). So when it tries to load the countries collection, it will go to the default store, which is the US, as described in my original post.

I don't know why processCountryOptions is called after filtering the currently selected country, and I don't know how the BackendQuoteSession object should be adjusted to properly select the store when editing orders.

I do know that as a workaround, if I comment out the code presented in item 1 above, and I comment out the loadByStore call in item 2 (to leave it out of the filter), the admin allows you to at least have all countries available (and the right one selected by default). That helps prevent Customer Support staff from accidentally corrupting existing non-US orders when trying to edit their addresses.

I was able to patch this so it works sufficiently by commenting out ->loadByStore($storeId) in processCountryOptions()

Was this page helpful?
0 / 5 - 0 ratings