AbstractCarrier.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Shipping\Model\Carrier;
  7. use Magento\Quote\Model\Quote\Address\RateResult\Error;
  8. use Magento\Shipping\Model\Shipment\Request;
  9. /**
  10. * Class AbstractCarrier
  11. *
  12. * @api
  13. * @since 100.0.2
  14. */
  15. abstract class AbstractCarrier extends \Magento\Framework\DataObject implements AbstractCarrierInterface
  16. {
  17. const DEBUG_KEYS_MASK = '****';
  18. /**
  19. * Carrier's code
  20. *
  21. * @var string
  22. */
  23. protected $_code;
  24. /**
  25. * Rates result
  26. *
  27. * @var array|null
  28. */
  29. protected $_rates;
  30. /**
  31. * Number of boxes in package
  32. *
  33. * @var int
  34. */
  35. protected $_numBoxes = 1;
  36. /**
  37. * Free Method config path
  38. *
  39. * @var string
  40. */
  41. protected $_freeMethod = 'free_method';
  42. /**
  43. * Whether this carrier has fixed rates calculation
  44. *
  45. * @var bool
  46. */
  47. protected $_isFixed = false;
  48. /**
  49. * Container types that could be customized
  50. *
  51. * @var string[]
  52. */
  53. protected $_customizableContainerTypes = [];
  54. const USA_COUNTRY_ID = 'US';
  55. const CANADA_COUNTRY_ID = 'CA';
  56. const MEXICO_COUNTRY_ID = 'MX';
  57. const HANDLING_TYPE_PERCENT = 'P';
  58. const HANDLING_TYPE_FIXED = 'F';
  59. const HANDLING_ACTION_PERPACKAGE = 'P';
  60. const HANDLING_ACTION_PERORDER = 'O';
  61. /**
  62. * Fields that should be replaced in debug with '***'
  63. *
  64. * @var array
  65. */
  66. protected $_debugReplacePrivateDataKeys = [];
  67. /**
  68. * Core store config
  69. *
  70. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  71. */
  72. protected $_scopeConfig;
  73. /**
  74. * @var \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory
  75. */
  76. protected $_rateErrorFactory;
  77. /**
  78. * @var \Psr\Log\LoggerInterface
  79. */
  80. protected $_logger;
  81. /**
  82. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  83. * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
  84. * @param \Psr\Log\LoggerInterface $logger
  85. * @param array $data
  86. */
  87. public function __construct(
  88. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  89. \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
  90. \Psr\Log\LoggerInterface $logger,
  91. array $data = []
  92. ) {
  93. parent::__construct($data);
  94. $this->_scopeConfig = $scopeConfig;
  95. $this->_rateErrorFactory = $rateErrorFactory;
  96. $this->_logger = $logger;
  97. }
  98. /**
  99. * Retrieve information from carrier configuration
  100. *
  101. * @param string $field
  102. * @return false|string
  103. */
  104. public function getConfigData($field)
  105. {
  106. if (empty($this->_code)) {
  107. return false;
  108. }
  109. $path = 'carriers/' . $this->_code . '/' . $field;
  110. return $this->_scopeConfig->getValue(
  111. $path,
  112. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  113. $this->getStore()
  114. );
  115. }
  116. /**
  117. * Retrieve config flag for store by field
  118. *
  119. * @param string $field
  120. * @return bool
  121. * @SuppressWarnings(PHPMD.BooleanGetMethodName)
  122. * @api
  123. */
  124. public function getConfigFlag($field)
  125. {
  126. if (empty($this->_code)) {
  127. return false;
  128. }
  129. $path = 'carriers/' . $this->_code . '/' . $field;
  130. return $this->_scopeConfig->isSetFlag(
  131. $path,
  132. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  133. $this->getStore()
  134. );
  135. }
  136. /**
  137. * Do request to shipment
  138. * Implementation must be in overridden method
  139. *
  140. * @param Request $request
  141. * @return \Magento\Framework\DataObject
  142. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  143. */
  144. public function requestToShipment($request)
  145. {
  146. return new \Magento\Framework\DataObject();
  147. }
  148. /**
  149. * Do return of shipment
  150. * Implementation must be in overridden method
  151. *
  152. * @param Request $request
  153. * @return \Magento\Framework\DataObject
  154. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  155. */
  156. public function returnOfShipment($request)
  157. {
  158. return new \Magento\Framework\DataObject();
  159. }
  160. /**
  161. * Return container types of carrier
  162. *
  163. * @param \Magento\Framework\DataObject|null $params
  164. * @return array
  165. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  166. */
  167. public function getContainerTypes(\Magento\Framework\DataObject $params = null)
  168. {
  169. return [];
  170. }
  171. /**
  172. * Get allowed containers of carrier
  173. *
  174. * @param \Magento\Framework\DataObject|null $params
  175. * @return array|bool
  176. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  177. * @SuppressWarnings(PHPMD.NPathComplexity)
  178. */
  179. protected function _getAllowedContainers(\Magento\Framework\DataObject $params = null)
  180. {
  181. $containersAll = $this->getContainerTypesAll();
  182. if (empty($containersAll)) {
  183. return [];
  184. }
  185. if (empty($params)) {
  186. return $containersAll;
  187. }
  188. $containersFilter = $this->getContainerTypesFilter();
  189. $containersFiltered = [];
  190. $method = $params->getMethod();
  191. $countryShipper = $params->getCountryShipper();
  192. $countryRecipient = $params->getCountryRecipient();
  193. if (empty($containersFilter)) {
  194. return $containersAll;
  195. }
  196. if (!$params || !$method || !$countryShipper || !$countryRecipient) {
  197. return $containersAll;
  198. }
  199. if ($countryShipper == self::USA_COUNTRY_ID && $countryRecipient == self::USA_COUNTRY_ID) {
  200. $direction = 'within_us';
  201. } else {
  202. if ($countryShipper == self::USA_COUNTRY_ID && $countryRecipient != self::USA_COUNTRY_ID) {
  203. $direction = 'from_us';
  204. } else {
  205. return $containersAll;
  206. }
  207. }
  208. foreach ($containersFilter as $dataItem) {
  209. $containers = $dataItem['containers'];
  210. $filters = $dataItem['filters'];
  211. if (!empty($filters[$direction]['method']) && in_array($method, $filters[$direction]['method'])) {
  212. foreach ($containers as $container) {
  213. if (!empty($containersAll[$container])) {
  214. $containersFiltered[$container] = $containersAll[$container];
  215. }
  216. }
  217. }
  218. }
  219. return !empty($containersFiltered) ? $containersFiltered : $containersAll;
  220. }
  221. /**
  222. * Get Container Types, that could be customized
  223. *
  224. * @return string[]
  225. */
  226. public function getCustomizableContainerTypes()
  227. {
  228. return $this->_customizableContainerTypes;
  229. }
  230. /**
  231. * Return delivery confirmation types of carrier
  232. *
  233. * @param \Magento\Framework\DataObject|null $params
  234. * @return array
  235. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  236. */
  237. public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $params = null)
  238. {
  239. return [];
  240. }
  241. /**
  242. * @param \Magento\Framework\DataObject $request
  243. * @return $this|bool|false|\Magento\Framework\Model\AbstractModel
  244. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  245. */
  246. public function checkAvailableShipCountries(\Magento\Framework\DataObject $request)
  247. {
  248. $speCountriesAllow = $this->getConfigData('sallowspecific');
  249. /*
  250. * for specific countries, the flag will be 1
  251. */
  252. if ($speCountriesAllow && $speCountriesAllow == 1) {
  253. $showMethod = $this->getConfigData('showmethod');
  254. $availableCountries = [];
  255. if ($this->getConfigData('specificcountry')) {
  256. $availableCountries = explode(',', $this->getConfigData('specificcountry'));
  257. }
  258. if ($availableCountries && in_array($request->getDestCountryId(), $availableCountries)) {
  259. return $this;
  260. } elseif ($showMethod && (!$availableCountries || $availableCountries && !in_array(
  261. $request->getDestCountryId(),
  262. $availableCountries
  263. ))
  264. ) {
  265. /** @var Error $error */
  266. $error = $this->_rateErrorFactory->create();
  267. $error->setCarrier($this->_code);
  268. $error->setCarrierTitle($this->getConfigData('title'));
  269. $errorMsg = $this->getConfigData('specificerrmsg');
  270. $error->setErrorMessage(
  271. $errorMsg ? $errorMsg : __(
  272. 'Sorry, but we can\'t deliver to the destination country with this shipping module.'
  273. )
  274. );
  275. return $error;
  276. } else {
  277. /*
  278. * The admin set not to show the shipping module if the delivery country
  279. * is not within specific countries
  280. */
  281. return false;
  282. }
  283. }
  284. return $this;
  285. }
  286. /**
  287. * Processing additional validation to check is carrier applicable.
  288. *
  289. * @param \Magento\Framework\DataObject $request
  290. * @return $this|bool|\Magento\Framework\DataObject
  291. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  292. * @since 100.2.6
  293. */
  294. public function processAdditionalValidation(\Magento\Framework\DataObject $request)
  295. {
  296. return $this;
  297. }
  298. /**
  299. * Processing additional validation to check is carrier applicable.
  300. *
  301. * @param \Magento\Framework\DataObject $request
  302. * @return $this|bool|\Magento\Framework\DataObject
  303. * @deprecated 100.2.6
  304. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  305. */
  306. public function proccessAdditionalValidation(\Magento\Framework\DataObject $request)
  307. {
  308. return $this->processAdditionalValidation($request);
  309. }
  310. /**
  311. * Determine whether current carrier enabled for activity
  312. *
  313. * @return bool
  314. */
  315. public function isActive()
  316. {
  317. $active = $this->getConfigData('active');
  318. return $active == 1 || $active == 'true';
  319. }
  320. /**
  321. * Whether this carrier has fixed rates calculation
  322. *
  323. * @return bool
  324. */
  325. public function isFixed()
  326. {
  327. return $this->_isFixed;
  328. }
  329. /**
  330. * Check if carrier has shipping tracking option available
  331. *
  332. * @return bool
  333. */
  334. public function isTrackingAvailable()
  335. {
  336. return false;
  337. }
  338. /**
  339. * Check if carrier has shipping label option available
  340. *
  341. * @return bool
  342. */
  343. public function isShippingLabelsAvailable()
  344. {
  345. return false;
  346. }
  347. /**
  348. * Retrieve sort order of current carrier
  349. *
  350. * @return string|null
  351. */
  352. public function getSortOrder()
  353. {
  354. return $this->getConfigData('sort_order');
  355. }
  356. /**
  357. * @param \Magento\Quote\Model\Quote\Address\RateRequest $request
  358. * @return void
  359. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  360. * @SuppressWarnings(PHPMD.NPathComplexity)
  361. */
  362. protected function _updateFreeMethodQuote($request)
  363. {
  364. if (!$request->getFreeShipping()) {
  365. return;
  366. }
  367. if ($request->getFreeMethodWeight() == $request->getPackageWeight() || !$request->hasFreeMethodWeight()) {
  368. return;
  369. }
  370. $freeMethod = $this->getConfigData($this->_freeMethod);
  371. if (!$freeMethod) {
  372. return;
  373. }
  374. $freeRateId = false;
  375. if (is_object($this->_result)) {
  376. foreach ($this->_result->getAllRates() as $i => $item) {
  377. if ($item->getMethod() == $freeMethod) {
  378. $freeRateId = $i;
  379. break;
  380. }
  381. }
  382. }
  383. if ($freeRateId === false) {
  384. return;
  385. }
  386. $price = null;
  387. if ($request->getFreeMethodWeight() > 0) {
  388. $this->_setFreeMethodRequest($freeMethod);
  389. $result = $this->_getQuotes();
  390. if ($result && ($rates = $result->getAllRates()) && count($rates) > 0) {
  391. if (count($rates) == 1 && $rates[0] instanceof \Magento\Quote\Model\Quote\Address\RateResult\Method) {
  392. $price = $rates[0]->getPrice();
  393. }
  394. if (count($rates) > 1) {
  395. foreach ($rates as $rate) {
  396. if ($rate instanceof \Magento\Quote\Model\Quote\Address\RateResult\Method &&
  397. $rate->getMethod() == $freeMethod
  398. ) {
  399. $price = $rate->getPrice();
  400. }
  401. }
  402. }
  403. }
  404. } else {
  405. /**
  406. * if we can apply free shipping for all order we should force price
  407. * to $0.00 for shipping with out sending second request to carrier
  408. */
  409. $price = 0;
  410. }
  411. /**
  412. * if we did not get our free shipping method in response we must use its old price
  413. */
  414. if ($price !== null) {
  415. $this->_result->getRateById($freeRateId)->setPrice($price);
  416. }
  417. }
  418. /**
  419. * Get the handling fee for the shipping + cost
  420. *
  421. * @param float $cost
  422. * @return float final price for shipping method
  423. */
  424. public function getFinalPriceWithHandlingFee($cost)
  425. {
  426. $handlingFee = (float)$this->getConfigData('handling_fee');
  427. $handlingType = $this->getConfigData('handling_type');
  428. if (!$handlingType) {
  429. $handlingType = self::HANDLING_TYPE_FIXED;
  430. }
  431. $handlingAction = $this->getConfigData('handling_action');
  432. if (!$handlingAction) {
  433. $handlingAction = self::HANDLING_ACTION_PERORDER;
  434. }
  435. return $handlingAction == self::HANDLING_ACTION_PERPACKAGE ? $this->_getPerpackagePrice(
  436. $cost,
  437. $handlingType,
  438. $handlingFee
  439. ) : $this->_getPerorderPrice(
  440. $cost,
  441. $handlingType,
  442. $handlingFee
  443. );
  444. }
  445. /**
  446. * Get final price for shipping method with handling fee per package
  447. *
  448. * @param float $cost
  449. * @param string $handlingType
  450. * @param float $handlingFee
  451. * @return float
  452. */
  453. protected function _getPerpackagePrice($cost, $handlingType, $handlingFee)
  454. {
  455. if ($handlingType == self::HANDLING_TYPE_PERCENT) {
  456. return ($cost + $cost * $handlingFee / 100) * $this->_numBoxes;
  457. }
  458. return ($cost + $handlingFee) * $this->_numBoxes;
  459. }
  460. /**
  461. * Get final price for shipping method with handling fee per order
  462. *
  463. * @param float $cost
  464. * @param string $handlingType
  465. * @param float $handlingFee
  466. * @return float
  467. */
  468. protected function _getPerorderPrice($cost, $handlingType, $handlingFee)
  469. {
  470. if ($handlingType == self::HANDLING_TYPE_PERCENT) {
  471. return $cost * $this->_numBoxes + $cost * $this->_numBoxes * $handlingFee / 100;
  472. }
  473. return $cost * $this->_numBoxes + $handlingFee;
  474. }
  475. /**
  476. * Sets the number of boxes for shipping
  477. *
  478. * @param int $weight in some measure
  479. * @return int
  480. */
  481. public function getTotalNumOfBoxes($weight)
  482. {
  483. /*
  484. reset num box first before retrieve again
  485. */
  486. $this->_numBoxes = 1;
  487. $maxPackageWeight = $this->getConfigData('max_package_weight');
  488. if ($weight > $maxPackageWeight && $maxPackageWeight != 0) {
  489. $this->_numBoxes = ceil($weight / $maxPackageWeight);
  490. $weight = $weight / $this->_numBoxes;
  491. }
  492. return $weight;
  493. }
  494. /**
  495. * Is state province required
  496. *
  497. * @return bool
  498. */
  499. public function isStateProvinceRequired()
  500. {
  501. return false;
  502. }
  503. /**
  504. * Check if city option required
  505. *
  506. * @return bool
  507. */
  508. public function isCityRequired()
  509. {
  510. return false;
  511. }
  512. /**
  513. * Determine whether zip-code is required for the country of destination
  514. *
  515. * @param string|null $countryId
  516. * @return bool
  517. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  518. */
  519. public function isZipCodeRequired($countryId = null)
  520. {
  521. return false;
  522. }
  523. /**
  524. * Log debug data to file
  525. *
  526. * @param mixed $debugData
  527. * @return void
  528. */
  529. protected function _debug($debugData)
  530. {
  531. if ($this->getDebugFlag()) {
  532. $this->_logger->debug(var_export($debugData, true));
  533. }
  534. }
  535. /**
  536. * Define if debugging is enabled
  537. *
  538. * @return bool
  539. * @SuppressWarnings(PHPMD.BooleanGetMethodName)
  540. * @api
  541. */
  542. public function getDebugFlag()
  543. {
  544. return $this->getConfigData('debug');
  545. }
  546. /**
  547. * Used to call debug method from not Payment Method context
  548. *
  549. * @param mixed $debugData
  550. * @return void
  551. */
  552. public function debugData($debugData)
  553. {
  554. $this->_debug($debugData);
  555. }
  556. /**
  557. * Getter for carrier code
  558. *
  559. * @return string
  560. */
  561. public function getCarrierCode()
  562. {
  563. return $this->_code;
  564. }
  565. /**
  566. * Return content types of package
  567. *
  568. * @param \Magento\Framework\DataObject $params
  569. * @return array
  570. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  571. */
  572. public function getContentTypes(\Magento\Framework\DataObject $params)
  573. {
  574. return [];
  575. }
  576. /**
  577. * Recursive replace sensitive fields of XML document.
  578. *
  579. * For example if xml document has the following structure:
  580. * ```xml
  581. * <Request>
  582. * <LicenseNumber>E437FJFD</LicenseNumber>
  583. * <UserId>testUser1</UserId>
  584. * <Password>userPassword</Password>
  585. * </Request>
  586. * ```
  587. * and sensitive fields are specified as `['UserId', 'Password']`, then sensitive fields
  588. * will be replaced by the mask(by default it is '****')
  589. *
  590. * @param string $data
  591. * @return string
  592. * @since 100.1.0
  593. */
  594. protected function filterDebugData($data)
  595. {
  596. try {
  597. $xml = new \SimpleXMLElement($data);
  598. $this->filterXmlData($xml);
  599. $data = $xml->asXML();
  600. } catch (\Exception $e) {
  601. }
  602. return $data;
  603. }
  604. /**
  605. * Recursive replace sensitive xml nodes values by specified mask
  606. * @param \SimpleXMLElement $xml
  607. * @return void
  608. */
  609. private function filterXmlData(\SimpleXMLElement $xml)
  610. {
  611. /** @var \SimpleXMLElement $child */
  612. foreach ($xml->children() as $child) {
  613. if ($child->count()) {
  614. $this->filterXmlData($child);
  615. } elseif (in_array((string) $child->getName(), $this->_debugReplacePrivateDataKeys)) {
  616. $child[0] = self::DEBUG_KEYS_MASK;
  617. }
  618. }
  619. }
  620. }