Checkout.php 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\Paypal\Model\Express;
  8. use Magento\Customer\Api\Data\CustomerInterface as CustomerDataObject;
  9. use Magento\Customer\Model\AccountManagement;
  10. use Magento\Framework\DataObject;
  11. use Magento\Paypal\Model\Cart as PaypalCart;
  12. use Magento\Paypal\Model\Config as PaypalConfig;
  13. use Magento\Quote\Model\Quote\Address;
  14. use Magento\Sales\Model\Order\Email\Sender\OrderSender;
  15. /**
  16. * Wrapper that performs Paypal Express and Checkout communication
  17. *
  18. * @SuppressWarnings(PHPMD.TooManyFields)
  19. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  20. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  21. * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
  22. */
  23. class Checkout
  24. {
  25. /**
  26. * Cache ID prefix for "pal" lookup
  27. *
  28. * @var string
  29. */
  30. const PAL_CACHE_ID = 'paypal_express_checkout_pal';
  31. /**
  32. * Keys for passthrough variables in sales/quote_payment and sales/order_payment
  33. * Uses additional_information as storage
  34. */
  35. const PAYMENT_INFO_TRANSPORT_TOKEN = 'paypal_express_checkout_token';
  36. const PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDDEN = 'paypal_express_checkout_shipping_overridden';
  37. const PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD = 'paypal_express_checkout_shipping_method';
  38. const PAYMENT_INFO_TRANSPORT_PAYER_ID = 'paypal_express_checkout_payer_id';
  39. const PAYMENT_INFO_TRANSPORT_REDIRECT = 'paypal_express_checkout_redirect_required';
  40. const PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT = 'paypal_ec_create_ba';
  41. /**
  42. * Flag which says that was used PayPal Express Checkout button for checkout
  43. * Uses additional_information as storage
  44. * @var string
  45. */
  46. const PAYMENT_INFO_BUTTON = 'button';
  47. /**
  48. * @var \Magento\Quote\Model\Quote
  49. */
  50. protected $_quote;
  51. /**
  52. * Config instance
  53. *
  54. * @var PaypalConfig
  55. */
  56. protected $_config;
  57. /**
  58. * API instance
  59. *
  60. * @var \Magento\Paypal\Model\Api\Nvp
  61. */
  62. protected $_api;
  63. /**
  64. * Api Model Type
  65. *
  66. * @var string
  67. */
  68. protected $_apiType = \Magento\Paypal\Model\Api\Nvp::class;
  69. /**
  70. * Payment method type
  71. *
  72. * @var string
  73. */
  74. protected $_methodType = PaypalConfig::METHOD_WPP_EXPRESS;
  75. /**
  76. * State helper variable
  77. *
  78. * @var string
  79. */
  80. protected $_redirectUrl = '';
  81. /**
  82. * State helper variable
  83. *
  84. * @var string
  85. */
  86. protected $_pendingPaymentMessage = '';
  87. /**
  88. * State helper variable
  89. *
  90. * @var string
  91. */
  92. protected $_checkoutRedirectUrl = '';
  93. /**
  94. * @var \Magento\Customer\Model\Session
  95. */
  96. protected $_customerSession;
  97. /**
  98. * Redirect urls supposed to be set to support giropay
  99. *
  100. * @var array
  101. */
  102. protected $_giropayUrls = [];
  103. /**
  104. * Create Billing Agreement flag
  105. *
  106. * @var bool
  107. */
  108. protected $_isBARequested = false;
  109. /**
  110. * Flag for Bill Me Later mode
  111. *
  112. * @var bool
  113. */
  114. protected $_isBml = false;
  115. /**
  116. * Customer ID
  117. *
  118. * @var int
  119. */
  120. protected $_customerId;
  121. /**
  122. * Billing agreement that might be created during order placing
  123. *
  124. * @var \Magento\Paypal\Model\Billing\Agreement
  125. */
  126. protected $_billingAgreement;
  127. /**
  128. * Order
  129. *
  130. * @var \Magento\Sales\Model\Order
  131. */
  132. protected $_order;
  133. /**
  134. * @var \Magento\Framework\App\Cache\Type\Config
  135. */
  136. protected $_configCacheType;
  137. /**
  138. * Checkout data
  139. *
  140. * @var \Magento\Checkout\Helper\Data
  141. */
  142. protected $_checkoutData;
  143. /**
  144. * Tax data
  145. *
  146. * @var \Magento\Tax\Helper\Data
  147. */
  148. protected $_taxData;
  149. /**
  150. * Customer data
  151. *
  152. * @var \Magento\Customer\Model\Url
  153. */
  154. protected $_customerUrl;
  155. /**
  156. * @var \Psr\Log\LoggerInterface
  157. */
  158. protected $_logger;
  159. /**
  160. * @var \Magento\Framework\Locale\ResolverInterface
  161. */
  162. protected $_localeResolver;
  163. /**
  164. * @var \Magento\Paypal\Model\Info
  165. */
  166. protected $_paypalInfo;
  167. /**
  168. * @var \Magento\Store\Model\StoreManagerInterface
  169. */
  170. protected $_storeManager;
  171. /**
  172. * @var \Magento\Framework\UrlInterface
  173. */
  174. protected $_coreUrl;
  175. /**
  176. * @var \Magento\Paypal\Model\CartFactory
  177. */
  178. protected $_cartFactory;
  179. /**
  180. * @var \Magento\Checkout\Model\Type\OnepageFactory
  181. */
  182. protected $_checkoutOnepageFactory;
  183. /**
  184. * @var \Magento\Paypal\Model\Billing\AgreementFactory
  185. */
  186. protected $_agreementFactory;
  187. /**
  188. * @var \Magento\Paypal\Model\Api\Type\Factory
  189. */
  190. protected $_apiTypeFactory;
  191. /**
  192. * @var \Magento\Framework\DataObject\Copy
  193. */
  194. protected $_objectCopyService;
  195. /**
  196. * @var \Magento\Checkout\Model\Session
  197. */
  198. protected $_checkoutSession;
  199. /**
  200. * @var \Magento\Customer\Api\CustomerRepositoryInterface
  201. */
  202. protected $_customerRepository;
  203. /**
  204. * @var \Magento\Customer\Model\AccountManagement
  205. */
  206. protected $_accountManagement;
  207. /**
  208. * @var \Magento\Framework\Encryption\EncryptorInterface
  209. */
  210. protected $_encryptor;
  211. /**
  212. * @var \Magento\Framework\Message\ManagerInterface
  213. */
  214. protected $_messageManager;
  215. /**
  216. * @var OrderSender
  217. */
  218. protected $orderSender;
  219. /**
  220. * @var \Magento\Quote\Api\CartRepositoryInterface
  221. */
  222. protected $quoteRepository;
  223. /**
  224. * @var \Magento\Quote\Api\CartManagementInterface
  225. */
  226. protected $quoteManagement;
  227. /**
  228. * @var \Magento\Quote\Model\Quote\TotalsCollector
  229. */
  230. protected $totalsCollector;
  231. /**
  232. * @param \Psr\Log\LoggerInterface $logger
  233. * @param \Magento\Customer\Model\Url $customerUrl
  234. * @param \Magento\Tax\Helper\Data $taxData
  235. * @param \Magento\Checkout\Helper\Data $checkoutData
  236. * @param \Magento\Customer\Model\Session $customerSession
  237. * @param \Magento\Framework\App\Cache\Type\Config $configCacheType
  238. * @param \Magento\Framework\Locale\ResolverInterface $localeResolver
  239. * @param \Magento\Paypal\Model\Info $paypalInfo
  240. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  241. * @param \Magento\Framework\UrlInterface $coreUrl
  242. * @param \Magento\Paypal\Model\CartFactory $cartFactory
  243. * @param \Magento\Checkout\Model\Type\OnepageFactory $onepageFactory
  244. * @param \Magento\Quote\Api\CartManagementInterface $quoteManagement
  245. * @param \Magento\Paypal\Model\Billing\AgreementFactory $agreementFactory
  246. * @param \Magento\Paypal\Model\Api\Type\Factory $apiTypeFactory
  247. * @param DataObject\Copy $objectCopyService
  248. * @param \Magento\Checkout\Model\Session $checkoutSession
  249. * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor
  250. * @param \Magento\Framework\Message\ManagerInterface $messageManager
  251. * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository
  252. * @param AccountManagement $accountManagement
  253. * @param OrderSender $orderSender
  254. * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
  255. * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector
  256. * @param array $params
  257. * @throws \Exception
  258. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  259. */
  260. public function __construct(
  261. \Psr\Log\LoggerInterface $logger,
  262. \Magento\Customer\Model\Url $customerUrl,
  263. \Magento\Tax\Helper\Data $taxData,
  264. \Magento\Checkout\Helper\Data $checkoutData,
  265. \Magento\Customer\Model\Session $customerSession,
  266. \Magento\Framework\App\Cache\Type\Config $configCacheType,
  267. \Magento\Framework\Locale\ResolverInterface $localeResolver,
  268. \Magento\Paypal\Model\Info $paypalInfo,
  269. \Magento\Store\Model\StoreManagerInterface $storeManager,
  270. \Magento\Framework\UrlInterface $coreUrl,
  271. \Magento\Paypal\Model\CartFactory $cartFactory,
  272. \Magento\Checkout\Model\Type\OnepageFactory $onepageFactory,
  273. \Magento\Quote\Api\CartManagementInterface $quoteManagement,
  274. \Magento\Paypal\Model\Billing\AgreementFactory $agreementFactory,
  275. \Magento\Paypal\Model\Api\Type\Factory $apiTypeFactory,
  276. \Magento\Framework\DataObject\Copy $objectCopyService,
  277. \Magento\Checkout\Model\Session $checkoutSession,
  278. \Magento\Framework\Encryption\EncryptorInterface $encryptor,
  279. \Magento\Framework\Message\ManagerInterface $messageManager,
  280. \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
  281. AccountManagement $accountManagement,
  282. OrderSender $orderSender,
  283. \Magento\Quote\Api\CartRepositoryInterface $quoteRepository,
  284. \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector,
  285. $params = []
  286. ) {
  287. $this->quoteManagement = $quoteManagement;
  288. $this->_customerUrl = $customerUrl;
  289. $this->_taxData = $taxData;
  290. $this->_checkoutData = $checkoutData;
  291. $this->_configCacheType = $configCacheType;
  292. $this->_logger = $logger;
  293. $this->_localeResolver = $localeResolver;
  294. $this->_paypalInfo = $paypalInfo;
  295. $this->_storeManager = $storeManager;
  296. $this->_coreUrl = $coreUrl;
  297. $this->_cartFactory = $cartFactory;
  298. $this->_checkoutOnepageFactory = $onepageFactory;
  299. $this->_agreementFactory = $agreementFactory;
  300. $this->_apiTypeFactory = $apiTypeFactory;
  301. $this->_objectCopyService = $objectCopyService;
  302. $this->_checkoutSession = $checkoutSession;
  303. $this->_customerRepository = $customerRepository;
  304. $this->_encryptor = $encryptor;
  305. $this->_messageManager = $messageManager;
  306. $this->orderSender = $orderSender;
  307. $this->_accountManagement = $accountManagement;
  308. $this->quoteRepository = $quoteRepository;
  309. $this->totalsCollector = $totalsCollector;
  310. $this->_customerSession = isset($params['session'])
  311. && $params['session'] instanceof \Magento\Customer\Model\Session ? $params['session'] : $customerSession;
  312. if (isset($params['config']) && $params['config'] instanceof PaypalConfig) {
  313. $this->_config = $params['config'];
  314. } else {
  315. throw new \Exception('Config instance is required.');
  316. }
  317. if (isset($params['quote']) && $params['quote'] instanceof \Magento\Quote\Model\Quote) {
  318. $this->_quote = $params['quote'];
  319. } else {
  320. throw new \Exception('Quote instance is required.');
  321. }
  322. }
  323. /**
  324. * Checkout with PayPal image URL getter
  325. *
  326. * Spares API calls of getting "pal" variable, by putting it into cache per store view
  327. *
  328. * @return string
  329. */
  330. public function getCheckoutShortcutImageUrl()
  331. {
  332. // get "pal" thing from cache or lookup it via API
  333. $pal = null;
  334. if ($this->_config->areButtonsDynamic()) {
  335. $cacheId = self::PAL_CACHE_ID . $this->_storeManager->getStore()->getId();
  336. $pal = $this->_configCacheType->load($cacheId);
  337. if (self::PAL_CACHE_ID == $pal) {
  338. $pal = null;
  339. } elseif (!$pal) {
  340. $pal = null;
  341. try {
  342. $this->_getApi()->callGetPalDetails();
  343. $pal = $this->_getApi()->getPal();
  344. $this->_configCacheType->save($pal, $cacheId);
  345. } catch (\Exception $e) {
  346. $this->_configCacheType->save(self::PAL_CACHE_ID, $cacheId);
  347. $this->_logger->critical($e);
  348. }
  349. }
  350. }
  351. return $this->_config->getExpressCheckoutShortcutImageUrl(
  352. $this->_localeResolver->getLocale(),
  353. $this->_quote->getBaseGrandTotal(),
  354. $pal
  355. );
  356. }
  357. /**
  358. * Setter that enables giropay redirects flow
  359. *
  360. * @param string $successUrl - payment success result
  361. * @param string $cancelUrl - payment cancellation result
  362. * @param string $pendingUrl - pending payment result
  363. * @return $this
  364. */
  365. public function prepareGiropayUrls($successUrl, $cancelUrl, $pendingUrl)
  366. {
  367. $this->_giropayUrls = [$successUrl, $cancelUrl, $pendingUrl];
  368. return $this;
  369. }
  370. /**
  371. * Set create billing agreement flag
  372. *
  373. * @param bool $flag
  374. * @return $this
  375. */
  376. public function setIsBillingAgreementRequested($flag)
  377. {
  378. $this->_isBARequested = $flag;
  379. return $this;
  380. }
  381. /**
  382. * Set flag that forces to use BillMeLater
  383. *
  384. * @param bool $isBml
  385. * @return $this
  386. */
  387. public function setIsBml($isBml)
  388. {
  389. $this->_isBml = $isBml;
  390. return $this;
  391. }
  392. /**
  393. * Setter for customer
  394. *
  395. * @param CustomerDataObject $customerData
  396. * @return $this
  397. */
  398. public function setCustomerData(CustomerDataObject $customerData)
  399. {
  400. $this->_quote->assignCustomer($customerData);
  401. $this->_customerId = $customerData->getId();
  402. return $this;
  403. }
  404. /**
  405. * Setter for customer with billing and shipping address changing ability
  406. *
  407. * @param CustomerDataObject $customerData
  408. * @param Address|null $billingAddress
  409. * @param Address|null $shippingAddress
  410. * @return $this
  411. */
  412. public function setCustomerWithAddressChange(
  413. CustomerDataObject $customerData,
  414. $billingAddress = null,
  415. $shippingAddress = null
  416. ) {
  417. $this->_quote->assignCustomerWithAddressChange($customerData, $billingAddress, $shippingAddress);
  418. $this->_customerId = $customerData->getId();
  419. return $this;
  420. }
  421. /**
  422. * Reserve order ID for specified quote and start checkout on PayPal
  423. *
  424. * @param string $returnUrl
  425. * @param string $cancelUrl
  426. * @param bool|null $button
  427. * @return string
  428. * @throws \Magento\Framework\Exception\LocalizedException
  429. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  430. * @SuppressWarnings(PHPMD.NPathComplexity)
  431. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  432. */
  433. public function start($returnUrl, $cancelUrl, $button = null)
  434. {
  435. $this->_quote->collectTotals();
  436. if (!$this->_quote->getGrandTotal()) {
  437. throw new \Magento\Framework\Exception\LocalizedException(
  438. __(
  439. 'PayPal can\'t process orders with a zero balance due. '
  440. . 'To finish your purchase, please go through the standard checkout process.'
  441. )
  442. );
  443. }
  444. $this->_quote->reserveOrderId();
  445. $this->quoteRepository->save($this->_quote);
  446. // prepare API
  447. $solutionType = $this->_config->getMerchantCountry() == 'DE'
  448. ? \Magento\Paypal\Model\Config::EC_SOLUTION_TYPE_MARK
  449. : $this->_config->getValue('solutionType');
  450. $totalAmount = round($this->_quote->getBaseGrandTotal(), 2);
  451. $this->_getApi()->setAmount($totalAmount)
  452. ->setCurrencyCode($this->_quote->getBaseCurrencyCode())
  453. ->setInvNum($this->_quote->getReservedOrderId())
  454. ->setReturnUrl($returnUrl)
  455. ->setCancelUrl($cancelUrl)
  456. ->setSolutionType($solutionType)
  457. ->setPaymentAction($this->_config->getValue('paymentAction'));
  458. if ($this->_giropayUrls) {
  459. list($successUrl, $cancelUrl, $pendingUrl) = $this->_giropayUrls;
  460. $this->_getApi()->addData(
  461. [
  462. 'giropay_cancel_url' => $cancelUrl,
  463. 'giropay_success_url' => $successUrl,
  464. 'giropay_bank_txn_pending_url' => $pendingUrl,
  465. ]
  466. );
  467. }
  468. if ($this->_isBml) {
  469. $this->_getApi()->setFundingSource('BML');
  470. }
  471. $this->_setBillingAgreementRequest();
  472. if ($this->_config->getValue('requireBillingAddress') == PaypalConfig::REQUIRE_BILLING_ADDRESS_ALL) {
  473. $this->_getApi()->setRequireBillingAddress(1);
  474. }
  475. // suppress or export shipping address
  476. $address = null;
  477. if ($this->_quote->getIsVirtual()) {
  478. if ($this->_config->getValue('requireBillingAddress')
  479. == PaypalConfig::REQUIRE_BILLING_ADDRESS_VIRTUAL
  480. ) {
  481. $this->_getApi()->setRequireBillingAddress(1);
  482. }
  483. $this->_getApi()->setSuppressShipping(true);
  484. } else {
  485. $this->_getApi()->setBillingAddress($this->_quote->getBillingAddress());
  486. $address = $this->_quote->getShippingAddress();
  487. $isOverridden = 0;
  488. if (true === $address->validate()) {
  489. $isOverridden = 1;
  490. $this->_getApi()->setAddress($address);
  491. }
  492. $this->_quote->getPayment()->setAdditionalInformation(
  493. self::PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDDEN,
  494. $isOverridden
  495. );
  496. $this->_quote->getPayment()->save();
  497. }
  498. /** @var $cart \Magento\Payment\Model\Cart */
  499. $cart = $this->_cartFactory->create(['salesModel' => $this->_quote]);
  500. $this->_getApi()->setPaypalCart($cart);
  501. if (!$this->_taxData->getConfig()->priceIncludesTax()) {
  502. $this->setShippingOptions($cart, $address);
  503. }
  504. $this->_config->exportExpressCheckoutStyleSettings($this->_getApi());
  505. /* Temporary solution. @TODO: do not pass quote into Nvp model */
  506. $this->_getApi()->setQuote($this->_quote);
  507. $this->_getApi()->callSetExpressCheckout();
  508. $token = $this->_getApi()->getToken();
  509. $this->_setRedirectUrl($button, $token);
  510. $payment = $this->_quote->getPayment();
  511. $payment->unsAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT);
  512. // Set flag that we came from Express Checkout button
  513. if (!empty($button)) {
  514. $payment->setAdditionalInformation(self::PAYMENT_INFO_BUTTON, 1);
  515. } elseif ($payment->hasAdditionalInformation(self::PAYMENT_INFO_BUTTON)) {
  516. $payment->unsAdditionalInformation(self::PAYMENT_INFO_BUTTON);
  517. }
  518. $payment->save();
  519. return $token;
  520. }
  521. /**
  522. * Check whether system can skip order review page before placing order
  523. *
  524. * @return bool
  525. */
  526. public function canSkipOrderReviewStep()
  527. {
  528. $isOnepageCheckout = !$this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON);
  529. return $this->_config->isOrderReviewStepDisabled() && $isOnepageCheckout;
  530. }
  531. /**
  532. * Update quote when returned from PayPal
  533. *
  534. * Rewrite billing address by paypal, save old billing address for new customer, and
  535. * export shipping address in case address absence
  536. *
  537. * @param string $token
  538. * @param string|null $payerIdentifier
  539. * @return void
  540. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  541. * @SuppressWarnings(PHPMD.NPathComplexity)
  542. */
  543. public function returnFromPaypal($token, string $payerIdentifier = null)
  544. {
  545. $this->_getApi()
  546. ->setToken($token)
  547. ->callGetExpressCheckoutDetails();
  548. $quote = $this->_quote;
  549. $this->ignoreAddressValidation();
  550. // check if we came from the Express Checkout button
  551. $isButton = (bool)$quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON);
  552. // import shipping address
  553. $exportedShippingAddress = $this->_getApi()->getExportedShippingAddress();
  554. if (!$quote->getIsVirtual()) {
  555. $shippingAddress = $quote->getShippingAddress();
  556. if ($shippingAddress) {
  557. if ($exportedShippingAddress && $isButton) {
  558. $this->_setExportedAddressData($shippingAddress, $exportedShippingAddress);
  559. // PayPal doesn't provide detailed shipping info: prefix, middlename, lastname, suffix
  560. $shippingAddress->setPrefix(null);
  561. $shippingAddress->setMiddlename(null);
  562. $shippingAddress->setLastname(null);
  563. $shippingAddress->setSuffix(null);
  564. $shippingAddress->setCollectShippingRates(true);
  565. $shippingAddress->setSameAsBilling(0);
  566. }
  567. // import shipping method
  568. $code = '';
  569. if ($this->_getApi()->getShippingRateCode()) {
  570. $code = $this->_matchShippingMethodCode($shippingAddress, $this->_getApi()->getShippingRateCode());
  571. if ($code) {
  572. // possible bug of double collecting rates :-/
  573. $shippingAddress->setShippingMethod($code)->setCollectShippingRates(true);
  574. }
  575. }
  576. $quote->getPayment()->setAdditionalInformation(
  577. self::PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD,
  578. $code
  579. );
  580. }
  581. }
  582. // import billing address
  583. $requireBillingAddress = (int)$this->_config->getValue(
  584. 'requireBillingAddress'
  585. ) === \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL;
  586. if ($isButton && !$requireBillingAddress && !$quote->isVirtual()) {
  587. $billingAddress = clone $shippingAddress;
  588. $billingAddress->unsAddressId()->unsAddressType()->setCustomerAddressId(null);
  589. $data = $billingAddress->getData();
  590. $data['save_in_address_book'] = 0;
  591. $quote->getBillingAddress()->addData($data);
  592. $quote->getShippingAddress()->setSameAsBilling(1);
  593. } else {
  594. $billingAddress = $quote->getBillingAddress()->setCustomerAddressId(null);
  595. }
  596. $exportedBillingAddress = $this->_getApi()->getExportedBillingAddress();
  597. // Since country is required field for billing and shipping address,
  598. // we consider the address information to be empty if country is empty.
  599. $isEmptyAddress = ($billingAddress->getCountryId() === null);
  600. if ($requireBillingAddress || $isEmptyAddress) {
  601. $this->_setExportedAddressData($billingAddress, $exportedBillingAddress);
  602. }
  603. $billingAddress->setCustomerNote($exportedBillingAddress->getData('note'));
  604. $quote->setBillingAddress($billingAddress);
  605. $quote->setCheckoutMethod($this->getCheckoutMethod());
  606. // import payment info
  607. $payment = $quote->getPayment();
  608. $payment->setMethod($this->_methodType);
  609. $this->_paypalInfo->importToPayment($this->_getApi(), $payment);
  610. $payerId = $payerIdentifier ? : $this->_getApi()->getPayerId();
  611. $payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $payerId)
  612. ->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_TOKEN, $token);
  613. $quote->collectTotals();
  614. $this->quoteRepository->save($quote);
  615. }
  616. /**
  617. * Check whether order review has enough data to initialize
  618. *
  619. * @param string|null $token
  620. * @return void
  621. * @throws \Magento\Framework\Exception\LocalizedException
  622. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  623. */
  624. public function prepareOrderReview($token = null)
  625. {
  626. $payment = $this->_quote->getPayment();
  627. if (!$payment || !$payment->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID)) {
  628. throw new \Magento\Framework\Exception\LocalizedException(__('A payer is not identified.'));
  629. }
  630. $this->_quote->setMayEditShippingAddress(
  631. 1 != $this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDDEN)
  632. );
  633. $this->_quote->setMayEditShippingMethod(
  634. '' == $this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD)
  635. );
  636. $this->ignoreAddressValidation();
  637. $this->_quote->collectTotals();
  638. $this->quoteRepository->save($this->_quote);
  639. }
  640. /**
  641. * Return callback response with shipping options
  642. *
  643. * @param array $request
  644. * @return string
  645. * @throws \Exception
  646. */
  647. public function getShippingOptionsCallbackResponse(array $request)
  648. {
  649. $debugData = ['request' => $request, 'response' => []];
  650. try {
  651. // obtain addresses
  652. $address = $this->_getApi()->prepareShippingOptionsCallbackAddress($request);
  653. $quoteAddress = $this->_quote->getShippingAddress();
  654. // compare addresses, calculate shipping rates and prepare response
  655. $options = [];
  656. if ($address && $quoteAddress && !$this->_quote->getIsVirtual()) {
  657. foreach ($address->getExportedKeys() as $key) {
  658. $quoteAddress->setDataUsingMethod($key, $address->getData($key));
  659. }
  660. $quoteAddress->setCollectShippingRates(true);
  661. $this->totalsCollector->collectAddressTotals($this->_quote, $quoteAddress);
  662. $options = $this->_prepareShippingOptions($quoteAddress, false, true);
  663. }
  664. $response = $this->_getApi()->setShippingOptions($options)->formatShippingOptionsCallback();
  665. // log request and response
  666. $debugData['response'] = $response;
  667. $this->_logger->debug(var_export($debugData, true));
  668. return $response;
  669. } catch (\Exception $e) {
  670. $this->_logger->debug(var_export($debugData, true));
  671. throw $e;
  672. }
  673. }
  674. /**
  675. * Set shipping method to quote, if needed
  676. *
  677. * @param string $methodCode
  678. * @return void
  679. */
  680. public function updateShippingMethod($methodCode)
  681. {
  682. $shippingAddress = $this->_quote->getShippingAddress();
  683. if (!$this->_quote->getIsVirtual() && $shippingAddress) {
  684. if ($methodCode != $shippingAddress->getShippingMethod()) {
  685. $this->ignoreAddressValidation();
  686. $shippingAddress->setShippingMethod($methodCode)->setCollectShippingRates(true);
  687. $cartExtension = $this->_quote->getExtensionAttributes();
  688. if ($cartExtension && $cartExtension->getShippingAssignments()) {
  689. $cartExtension->getShippingAssignments()[0]
  690. ->getShipping()
  691. ->setMethod($methodCode);
  692. }
  693. $this->_quote->collectTotals();
  694. $this->quoteRepository->save($this->_quote);
  695. }
  696. }
  697. }
  698. /**
  699. * Place the order when customer returned from PayPal until this moment all quote data must be valid.
  700. *
  701. * @param string $token
  702. * @param string|null $shippingMethodCode
  703. * @return void
  704. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  705. * @SuppressWarnings(PHPMD.NPathComplexity)
  706. */
  707. public function place($token, $shippingMethodCode = null)
  708. {
  709. if ($shippingMethodCode) {
  710. $this->updateShippingMethod($shippingMethodCode);
  711. }
  712. if ($this->getCheckoutMethod() == \Magento\Checkout\Model\Type\Onepage::METHOD_GUEST) {
  713. $this->prepareGuestQuote();
  714. }
  715. $this->ignoreAddressValidation();
  716. $this->_quote->collectTotals();
  717. $order = $this->quoteManagement->submit($this->_quote);
  718. if (!$order) {
  719. return;
  720. }
  721. // commence redirecting to finish payment, if paypal requires it
  722. if ($order->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_REDIRECT)) {
  723. $this->_redirectUrl = $this->_config->getExpressCheckoutCompleteUrl($token);
  724. }
  725. switch ($order->getState()) {
  726. // even after placement paypal can disallow to authorize/capture, but will wait until bank transfers money
  727. case \Magento\Sales\Model\Order::STATE_PENDING_PAYMENT:
  728. // TODO
  729. break;
  730. // regular placement, when everything is ok
  731. case \Magento\Sales\Model\Order::STATE_PROCESSING:
  732. case \Magento\Sales\Model\Order::STATE_COMPLETE:
  733. case \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW:
  734. try {
  735. if (!$order->getEmailSent()) {
  736. $this->orderSender->send($order);
  737. }
  738. } catch (\Exception $e) {
  739. $this->_logger->critical($e);
  740. }
  741. $this->_checkoutSession->start();
  742. break;
  743. default:
  744. break;
  745. }
  746. $this->_order = $order;
  747. }
  748. /**
  749. * Make sure addresses will be saved without validation errors
  750. *
  751. * @return void
  752. */
  753. private function ignoreAddressValidation()
  754. {
  755. $this->_quote->getBillingAddress()->setShouldIgnoreValidation(true);
  756. if (!$this->_quote->getIsVirtual()) {
  757. $this->_quote->getShippingAddress()->setShouldIgnoreValidation(true);
  758. if (!$this->_config->getValue('requireBillingAddress')
  759. && !$this->_quote->getBillingAddress()->getEmail()
  760. ) {
  761. $this->_quote->getBillingAddress()->setSameAsBilling(1);
  762. }
  763. }
  764. }
  765. /**
  766. * Determine whether redirect somewhere specifically is required
  767. *
  768. * @return string
  769. */
  770. public function getRedirectUrl()
  771. {
  772. return $this->_redirectUrl;
  773. }
  774. /**
  775. * Get created billing agreement
  776. *
  777. * @return \Magento\Paypal\Model\Billing\Agreement|null
  778. */
  779. public function getBillingAgreement()
  780. {
  781. return $this->_billingAgreement;
  782. }
  783. /**
  784. * Return order
  785. *
  786. * @return \Magento\Sales\Model\Order
  787. */
  788. public function getOrder()
  789. {
  790. return $this->_order;
  791. }
  792. /**
  793. * Get checkout method
  794. *
  795. * @return string
  796. */
  797. public function getCheckoutMethod()
  798. {
  799. if ($this->getCustomerSession()->isLoggedIn()) {
  800. return \Magento\Checkout\Model\Type\Onepage::METHOD_CUSTOMER;
  801. }
  802. if (!$this->_quote->getCheckoutMethod()) {
  803. if ($this->_checkoutData->isAllowedGuestCheckout($this->_quote)) {
  804. $this->_quote->setCheckoutMethod(\Magento\Checkout\Model\Type\Onepage::METHOD_GUEST);
  805. } else {
  806. $this->_quote->setCheckoutMethod(\Magento\Checkout\Model\Type\Onepage::METHOD_REGISTER);
  807. }
  808. }
  809. return $this->_quote->getCheckoutMethod();
  810. }
  811. /**
  812. * Sets address data from exported address
  813. *
  814. * @param Address $address
  815. * @param array $exportedAddress
  816. * @return void
  817. */
  818. protected function _setExportedAddressData($address, $exportedAddress)
  819. {
  820. foreach ($exportedAddress->getExportedKeys() as $key) {
  821. $data = $exportedAddress->getData($key);
  822. if (!empty($data)) {
  823. $address->setDataUsingMethod($key, $data);
  824. }
  825. }
  826. }
  827. /**
  828. * Set create billing agreement flag to api call
  829. *
  830. * @return $this
  831. */
  832. protected function _setBillingAgreementRequest()
  833. {
  834. if (!$this->_customerId) {
  835. return $this;
  836. }
  837. $isRequested = $this->_isBARequested || $this->_quote->getPayment()
  838. ->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT);
  839. if (!($this->_config->getValue('allow_ba_signup') == PaypalConfig::EC_BA_SIGNUP_AUTO
  840. || $isRequested && $this->_config->shouldAskToCreateBillingAgreement())
  841. ) {
  842. return $this;
  843. }
  844. if (!$this->_agreementFactory->create()->needToCreateForCustomer($this->_customerId)) {
  845. return $this;
  846. }
  847. $this->_getApi()->setBillingType($this->_getApi()->getBillingAgreementType());
  848. return $this;
  849. }
  850. /**
  851. * Get api
  852. *
  853. * @return \Magento\Paypal\Model\Api\Nvp
  854. */
  855. protected function _getApi()
  856. {
  857. if (null === $this->_api) {
  858. $this->_api = $this->_apiTypeFactory->create($this->_apiType)->setConfigObject($this->_config);
  859. }
  860. return $this->_api;
  861. }
  862. /**
  863. * Attempt to collect address shipping rates and return them for further usage in instant update API
  864. *
  865. * Returns empty array if it was impossible to obtain any shipping rate and
  866. * if there are shipping rates obtained, the method must return one of them as default.
  867. *
  868. * @param Address $address
  869. * @param bool $mayReturnEmpty
  870. * @param bool $calculateTax
  871. * @return array|false
  872. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  873. * @SuppressWarnings(PHPMD.NPathComplexity)
  874. */
  875. protected function _prepareShippingOptions(Address $address, $mayReturnEmpty = false, $calculateTax = false)
  876. {
  877. $options = [];
  878. $i = 0;
  879. $iMin = false;
  880. $min = false;
  881. $userSelectedOption = null;
  882. foreach ($address->getGroupedAllShippingRates() as $group) {
  883. foreach ($group as $rate) {
  884. $amount = (double)$rate->getPrice();
  885. if ($rate->getErrorMessage()) {
  886. continue;
  887. }
  888. $isDefault = $address->getShippingMethod() === $rate->getCode();
  889. $amountExclTax = $this->_taxData->getShippingPrice($amount, false, $address);
  890. $amountInclTax = $this->_taxData->getShippingPrice($amount, true, $address);
  891. $options[$i] = new \Magento\Framework\DataObject(
  892. [
  893. 'is_default' => $isDefault,
  894. 'name' => trim("{$rate->getCarrierTitle()} - {$rate->getMethodTitle()}", ' -'),
  895. 'code' => $rate->getCode(),
  896. 'amount' => $amountExclTax,
  897. ]
  898. );
  899. if ($calculateTax) {
  900. $options[$i]->setTaxAmount(
  901. $amountInclTax - $amountExclTax + $address->getTaxAmount() - $address->getShippingTaxAmount()
  902. );
  903. }
  904. if ($isDefault) {
  905. $userSelectedOption = $options[$i];
  906. }
  907. if (false === $min || $amountInclTax < $min) {
  908. $min = $amountInclTax;
  909. $iMin = $i;
  910. }
  911. $i++;
  912. }
  913. }
  914. if ($mayReturnEmpty && $userSelectedOption === null) {
  915. $options[] = new \Magento\Framework\DataObject(
  916. [
  917. 'is_default' => true,
  918. 'name' => __('N/A'),
  919. 'code' => 'no_rate',
  920. 'amount' => 0.00,
  921. ]
  922. );
  923. if ($calculateTax) {
  924. $options[$i]->setTaxAmount($address->getTaxAmount());
  925. }
  926. } elseif ($userSelectedOption === null && isset($options[$iMin])) {
  927. $options[$iMin]->setIsDefault(true);
  928. }
  929. // Magento will transfer only first 10 cheapest shipping options if there are more than 10 available.
  930. if (count($options) > 10) {
  931. usort($options, [get_class($this), 'cmpShippingOptions']);
  932. array_splice($options, 10);
  933. // User selected option will be always included in options list
  934. if ($userSelectedOption !== null && !in_array($userSelectedOption, $options)) {
  935. $options[9] = $userSelectedOption;
  936. }
  937. }
  938. return $options;
  939. }
  940. /**
  941. * Compare two shipping options based on their amounts
  942. *
  943. * This function is used as a callback comparison function in shipping options sorting process
  944. *
  945. * @see self::_prepareShippingOptions()
  946. * @param \Magento\Framework\DataObject $option1
  947. * @param \Magento\Framework\DataObject $option2
  948. * @return int
  949. */
  950. protected static function cmpShippingOptions(DataObject $option1, DataObject $option2)
  951. {
  952. return $option1->getAmount() <=> $option2->getAmount();
  953. }
  954. /**
  955. * Try to find whether the code provided by PayPal corresponds to any of possible shipping rates
  956. *
  957. * This method was created only because PayPal has issues with returning the selected code.
  958. * If in future the issue is fixed, we don't need to attempt to match it. It would be enough to set the method code
  959. * before collecting shipping rates
  960. *
  961. * @param Address $address
  962. * @param string $selectedCode
  963. * @return string
  964. */
  965. protected function _matchShippingMethodCode(Address $address, $selectedCode)
  966. {
  967. $options = $this->_prepareShippingOptions($address, false);
  968. foreach ($options as $option) {
  969. if ($selectedCode === $option['code'] // the proper case as outlined in documentation
  970. || $selectedCode === $option['name'] // workaround: PayPal may return name instead of the code
  971. // workaround: PayPal may concatenate code and name, and return it instead of the code:
  972. || $selectedCode === "{$option['code']} {$option['name']}"
  973. ) {
  974. return $option['code'];
  975. }
  976. }
  977. return '';
  978. }
  979. /**
  980. * Create payment redirect url
  981. *
  982. * @param bool|null $button
  983. * @param string $token
  984. * @return void
  985. */
  986. protected function _setRedirectUrl($button, $token)
  987. {
  988. $this->_redirectUrl = ($button && !$this->_taxData->getConfig()->priceIncludesTax())
  989. ? $this->_config->getExpressCheckoutStartUrl($token)
  990. : $this->_config->getPayPalBasicStartUrl($token);
  991. }
  992. /**
  993. * Get customer session object
  994. *
  995. * @return \Magento\Customer\Model\Session
  996. */
  997. public function getCustomerSession()
  998. {
  999. return $this->_customerSession;
  1000. }
  1001. /**
  1002. * Set shipping options to api
  1003. *
  1004. * @param \Magento\Paypal\Model\Cart $cart
  1005. * @param \Magento\Quote\Model\Quote\Address|null $address
  1006. * @return void
  1007. */
  1008. private function setShippingOptions(PaypalCart $cart, Address $address = null)
  1009. {
  1010. // for included tax always disable line items (related to paypal amount rounding problem)
  1011. $this->_getApi()->setIsLineItemsEnabled($this->_config->getValue(PaypalConfig::TRANSFER_CART_LINE_ITEMS));
  1012. // add shipping options if needed and line items are available
  1013. $cartItems = $cart->getAllItems();
  1014. if ($this->_config->getValue(PaypalConfig::TRANSFER_CART_LINE_ITEMS)
  1015. && $this->_config->getValue(PaypalConfig::TRANSFER_SHIPPING_OPTIONS)
  1016. && !empty($cartItems)
  1017. ) {
  1018. if (!$this->_quote->getIsVirtual()) {
  1019. $options = $this->_prepareShippingOptions($address, true);
  1020. if ($options) {
  1021. $this->_getApi()->setShippingOptionsCallbackUrl(
  1022. $this->_coreUrl->getUrl(
  1023. '*/*/shippingOptionsCallback',
  1024. ['quote_id' => $this->_quote->getId()]
  1025. )
  1026. )->setShippingOptions($options);
  1027. }
  1028. }
  1029. }
  1030. }
  1031. /**
  1032. * Prepare quote for guest checkout order submit
  1033. *
  1034. * @return $this
  1035. */
  1036. protected function prepareGuestQuote()
  1037. {
  1038. $quote = $this->_quote;
  1039. $quote->setCustomerId(null)
  1040. ->setCustomerEmail($quote->getBillingAddress()->getEmail())
  1041. ->setCustomerIsGuest(true)
  1042. ->setCustomerGroupId(\Magento\Customer\Model\Group::NOT_LOGGED_IN_ID);
  1043. return $this;
  1044. }
  1045. }