Skip to content

Commit da68586

Browse files
committed
MAGETWO-71522: [Backport] - Checkout using Paypal with configurable product fails and increases QTY of both configurable and simple products - for 2.1
1 parent 409eef5 commit da68586

File tree

4 files changed

+270
-17
lines changed

4 files changed

+270
-17
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Paypal\Model\Express;
7+
8+
use Magento\Quote\Model\QuoteRepository\SaveHandler;
9+
use Magento\Quote\Api\Data\CartInterface;
10+
use Magento\Quote\Model\Quote\ProductOptionFactory;
11+
12+
/**
13+
* Plugin for Magento\Quote\Model\QuoteRepository\SaveHandler.
14+
*
15+
* Replaces cart item product options for disabled quote
16+
* which prevents it to be processed after placement of order
17+
* via PayPal Express payment solution.
18+
*/
19+
class QuotePlugin
20+
{
21+
/**
22+
* @var ProductOptionFactory
23+
*/
24+
private $productOptionFactory;
25+
26+
/**
27+
* @param ProductOptionFactory $productOptionFactory
28+
*/
29+
public function __construct(ProductOptionFactory $productOptionFactory)
30+
{
31+
$this->productOptionFactory = $productOptionFactory;
32+
}
33+
34+
/**
35+
* Replace cart item product options for disabled quote.
36+
*
37+
* @param SaveHandler $subject
38+
* @param CartInterface $quote
39+
* @return array
40+
*
41+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
42+
*/
43+
public function beforeSave(SaveHandler $subject, CartInterface $quote)
44+
{
45+
if (!$quote->getIsActive()) {
46+
$items = $quote->getItems();
47+
48+
if ($items) {
49+
foreach ($items as $item) {
50+
/** @var \Magento\Quote\Model\Quote\Item $item */
51+
if (!$item->isDeleted()) {
52+
$item->setProductOption($this->productOptionFactory->create());
53+
}
54+
}
55+
}
56+
}
57+
58+
return [$quote];
59+
}
60+
}

app/code/Magento/Paypal/etc/frontend/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,7 @@
112112
</argument>
113113
</arguments>
114114
</type>
115+
<type name="Magento\Quote\Model\QuoteRepository\SaveHandler">
116+
<plugin name="paypal-cartitem" type="Magento\Paypal\Model\Express\QuotePlugin"/>
117+
</type>
115118
</config>

dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55
*/
66
namespace Magento\Paypal\Controller;
77

8+
use Magento\Checkout\Model\Session;
9+
use Magento\Framework\Session\Generic as GenericSession;
10+
use Magento\Paypal\Model\Api\Nvp;
11+
use Magento\Paypal\Model\Api\Type\Factory as ApiFactory;
12+
use Magento\Paypal\Model\Session as PaypalSession;
13+
use Magento\Quote\Model\Quote;
14+
use Magento\TestFramework\Helper\Bootstrap;
15+
16+
/**
17+
* Tests of Paypal Express actions.
18+
*
19+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
20+
*/
821
class ExpressTest extends \Magento\TestFramework\TestCase\AbstractController
922
{
1023
/**
@@ -13,10 +26,10 @@ class ExpressTest extends \Magento\TestFramework\TestCase\AbstractController
1326
*/
1427
public function testReviewAction()
1528
{
16-
$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Quote\Model\Quote');
29+
$quote = Bootstrap::getObjectManager()->create(Quote::class);
1730
$quote->load('test01', 'reserved_order_id');
18-
\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
19-
'Magento\Checkout\Model\Session'
31+
Bootstrap::getObjectManager()->get(
32+
Session::class
2033
)->setQuoteId(
2134
$quote->getId()
2235
);
@@ -30,16 +43,16 @@ public function testReviewAction()
3043
}
3144

3245
/**
33-
* @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php
46+
* @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php
3447
* @magentoConfigFixture current_store paypal/general/business_account merchant_2012050718_biz@example.com
3548
*/
3649
public function testCancelAction()
3750
{
38-
$quote = $this->_objectManager->create('Magento\Quote\Model\Quote');
51+
$quote = $this->_objectManager->create(Quote::class);
3952
$quote->load('100000002', 'reserved_order_id');
40-
$order = $this->_objectManager->create('Magento\Sales\Model\Order');
53+
$order = $this->_objectManager->create(\Magento\Sales\Model\Order::class);
4154
$order->load('100000002', 'increment_id');
42-
$session = $this->_objectManager->get('Magento\Checkout\Model\Session');
55+
$session = $this->_objectManager->get(Session::class);
4356
$session->setLoadInactive(true);
4457
$session->setLastRealOrderId(
4558
$order->getRealOrderId()
@@ -50,8 +63,8 @@ public function testCancelAction()
5063
)->setQuoteId(
5164
$order->getQuoteId()
5265
);
53-
/** @var $paypalSession \Magento\Framework\Session\Generic */
54-
$paypalSession = $this->_objectManager->get('Magento\Paypal\Model\Session');
66+
/** @var GenericSession $paypalSession */
67+
$paypalSession = $this->_objectManager->get(PaypalSession::class);
5568
$paypalSession->setExpressCheckoutToken('token');
5669

5770
$this->dispatch('paypal/express/cancel');
@@ -79,18 +92,18 @@ public function testStartActionCustomerToQuote()
7992

8093
/** Preconditions */
8194
/** @var \Magento\Customer\Model\Session $customerSession */
82-
$customerSession = $this->_objectManager->get('Magento\Customer\Model\Session');
95+
$customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class);
8396
/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */
84-
$customerRepository = $this->_objectManager->get('Magento\Customer\Api\CustomerRepositoryInterface');
97+
$customerRepository = $this->_objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class);
8598
$customerData = $customerRepository->getById($fixtureCustomerId);
8699
$customerSession->setCustomerDataObject($customerData);
87100

88-
/** @var \Magento\Quote\Model\Quote $quote */
89-
$quote = $this->_objectManager->create('Magento\Quote\Model\Quote');
101+
/** @var Quote $quote */
102+
$quote = $this->_objectManager->create(Quote::class);
90103
$quote->load($fixtureQuoteReserveId, 'reserved_order_id');
91104

92-
/** @var \Magento\Checkout\Model\Session $checkoutSession */
93-
$checkoutSession = $this->_objectManager->get('Magento\Checkout\Model\Session');
105+
/** @var Session $checkoutSession */
106+
$checkoutSession = $this->_objectManager->get(Session::class);
94107
$checkoutSession->setQuoteId($quote->getId());
95108

96109
/** Preconditions check */
@@ -109,8 +122,8 @@ public function testStartActionCustomerToQuote()
109122
$this->dispatch('paypal/express/start');
110123

111124
/** Check if customer data was copied to quote correctly */
112-
/** @var \Magento\Quote\Model\Quote $updatedQuote */
113-
$updatedQuote = $this->_objectManager->create('Magento\Quote\Model\Quote');
125+
/** @var Quote $updatedQuote */
126+
$updatedQuote = $this->_objectManager->create(Quote::class);
114127
$updatedQuote->load($fixtureQuoteReserveId, 'reserved_order_id');
115128
$this->assertEquals(
116129
$fixtureCustomerEmail,
@@ -123,4 +136,118 @@ public function testStartActionCustomerToQuote()
123136
"Customer first name in quote is invalid."
124137
);
125138
}
139+
140+
/**
141+
* Test return action with configurable product.
142+
*
143+
* @magentoDataFixture Magento/Paypal/_files/quote_express_configurable.php
144+
* @magentoDbIsolation enabled
145+
* @magentoAppIsolation enabled
146+
*/
147+
public function testReturnAction()
148+
{
149+
/** @var Quote $quote */
150+
$quote = $this->_objectManager->create(Quote::class);
151+
$quote->load('test_cart_with_configurable', 'reserved_order_id');
152+
153+
$payment = $quote->getPayment();
154+
$payment->setMethod(\Magento\Paypal\Model\Config::METHOD_WPP_EXPRESS)
155+
->setAdditionalInformation(\Magento\Paypal\Model\Express\Checkout::PAYMENT_INFO_TRANSPORT_PAYER_ID, 123);
156+
157+
$quote->save();
158+
159+
$this->_objectManager->removeSharedInstance(Session::class);
160+
$session = $this->_objectManager->get(Session::class);
161+
$session->setQuoteId($quote->getId());
162+
163+
$nvpMethods = [
164+
'setToken',
165+
'setPayerId',
166+
'setAmount',
167+
'setPaymentAction',
168+
'setNotifyUrl',
169+
'setInvNum',
170+
'setCurrencyCode',
171+
'setPaypalCart',
172+
'setIsLineItemsEnabled',
173+
'setAddress',
174+
'setBillingAddress',
175+
'callDoExpressCheckoutPayment',
176+
'callGetExpressCheckoutDetails',
177+
];
178+
179+
$nvpMock = $this->getMockBuilder(Nvp::class)
180+
->setMethods($nvpMethods)
181+
->disableOriginalConstructor()
182+
->getMock();
183+
184+
foreach ($nvpMethods as $method) {
185+
$nvpMock->method($method)
186+
->willReturnSelf();
187+
}
188+
189+
$exportedBillingAddress = $this->getExportedAddressFixture($quote->getBillingAddress()->toArray());
190+
$nvpMock->setData('exported_billing_address', $exportedBillingAddress);
191+
192+
$apiFactoryMock = $this->getMockBuilder(ApiFactory::class)
193+
->disableOriginalConstructor()
194+
->setMethods(['create'])
195+
->getMock();
196+
197+
$apiFactoryMock->method('create')
198+
->with(Nvp::class)
199+
->willReturn($nvpMock);
200+
201+
$this->_objectManager->addSharedInstance($apiFactoryMock, ApiFactory::class);
202+
203+
$sessionMock = $this->getMockBuilder(GenericSession::class)
204+
->setMethods(['getExpressCheckoutToken'])
205+
->setConstructorArgs(
206+
[
207+
$this->_objectManager->get(\Magento\Framework\App\Request\Http::class),
208+
$this->_objectManager->get(\Magento\Framework\Session\SidResolverInterface::class),
209+
$this->_objectManager->get(\Magento\Framework\Session\Config\ConfigInterface::class),
210+
$this->_objectManager->get(\Magento\Framework\Session\SaveHandlerInterface::class),
211+
$this->_objectManager->get(\Magento\Framework\Session\ValidatorInterface::class),
212+
$this->_objectManager->get(\Magento\Framework\Session\StorageInterface::class),
213+
$this->_objectManager->get(\Magento\Framework\Stdlib\CookieManagerInterface::class),
214+
$this->_objectManager->get(\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class),
215+
$this->_objectManager->get(\Magento\Framework\App\State::class),
216+
]
217+
)
218+
->getMock();
219+
220+
$sessionMock->method('getExpressCheckoutToken')
221+
->willReturn(true);
222+
223+
$this->_objectManager->addSharedInstance($sessionMock, PaypalSession::class);
224+
225+
$this->dispatch('paypal/express/returnAction');
226+
$this->assertRedirect($this->stringContains('checkout/onepage/success'));
227+
228+
$this->_objectManager->removeSharedInstance(ApiFactory::class);
229+
$this->_objectManager->removeSharedInstance(PaypalSession::class);
230+
}
231+
232+
/**
233+
* Prepare fixture for exported address.
234+
*
235+
* @param array $addressData
236+
* @return \Magento\Framework\DataObject
237+
*/
238+
private function getExportedAddressFixture(array $addressData)
239+
{
240+
$addressDataKeys = ['firstname', 'lastname', 'street', 'city', 'telephone'];
241+
$result = [];
242+
foreach ($addressDataKeys as $key) {
243+
if (isset($addressData[$key])) {
244+
$result[$key] = 'exported' . $addressData[$key];
245+
}
246+
}
247+
$fixture = new \Magento\Framework\DataObject($result);
248+
$fixture->setExportedKeys($addressDataKeys);
249+
$fixture->setData('note', 'note');
250+
251+
return $fixture;
252+
}
126253
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
use Magento\Catalog\Api\ProductRepositoryInterface;
8+
use Magento\Checkout\Model\Cart;
9+
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection;
10+
use Magento\Quote\Model\Quote\Address;
11+
use Magento\Quote\Model\Quote\Address\Rate;
12+
use Magento\TestFramework\Helper\Bootstrap;
13+
14+
require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php';
15+
16+
/** @var $objectManager \Magento\TestFramework\ObjectManager */
17+
$objectManager = Bootstrap::getObjectManager();
18+
19+
/** @var \Magento\Catalog\Model\Product $product */
20+
/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
21+
$productRepository = $objectManager->create(ProductRepositoryInterface::class);
22+
$product = $productRepository->get('configurable');
23+
24+
/** @var $options Collection */
25+
$options = $objectManager->create(Collection::class);
26+
$option = $options->setAttributeFilter($attribute->getId())->getFirstItem();
27+
28+
$requestInfo = new \Magento\Framework\DataObject(
29+
[
30+
'product' => 1,
31+
'selected_configurable_option' => 1,
32+
'qty' => 100,
33+
'super_attribute' => [
34+
$attribute->getId() => $option->getId(),
35+
],
36+
]
37+
);
38+
39+
/** @var Cart $cart */
40+
$cart = $objectManager->create(Cart::class);
41+
$cart->addProduct($product, $requestInfo);
42+
43+
/** @var Rate $rate */
44+
$rate = $objectManager->create(Rate::class);
45+
$rate->setCode('flatrate_flatrate');
46+
$rate->setPrice(1);
47+
48+
$addressData = include __DIR__ . '/address_data.php';
49+
$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]);
50+
$billingAddress->setAddressType('billing');
51+
52+
$shippingAddress = clone $billingAddress;
53+
$shippingAddress->setId(null)
54+
->setAddressType('shipping')
55+
->setShippingMethod('flatrate_flatrate')
56+
->addShippingRate($rate);
57+
58+
$cart->getQuote()
59+
->setReservedOrderId('test_cart_with_configurable')
60+
->setBillingAddress($billingAddress)
61+
->setShippingAddress($shippingAddress);
62+
63+
$cart->save();

0 commit comments

Comments
 (0)