ShipmentProcessor.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * Refer to LICENSE.txt distributed with the Temando Shipping module for notice of license
  4. */
  5. namespace Temando\Shipping\Sync;
  6. use Magento\Framework\Exception\LocalizedException;
  7. use Magento\Framework\Exception\NoSuchEntityException;
  8. use Magento\Sales\Api\Data\OrderInterface;
  9. use Magento\Sales\Api\Data\ShipmentCreationArgumentsExtensionInterfaceFactory;
  10. use Magento\Sales\Api\Data\ShipmentCreationArgumentsInterfaceFactory;
  11. use Magento\Sales\Api\Data\ShipmentItemCreationInterface;
  12. use Magento\Sales\Api\Data\ShipmentItemCreationInterfaceFactory;
  13. use Magento\Sales\Api\Data\ShipmentTrackCreationInterfaceFactory;
  14. use Magento\Sales\Api\OrderRepositoryInterface;
  15. use Magento\Sales\Api\ShipmentRepositoryInterface as SalesShipmentRepositoryInterface;
  16. use Magento\Sales\Api\ShipOrderInterface;
  17. use Temando\Shipping\Model\DocumentationInterface;
  18. use Temando\Shipping\Model\ResourceModel\Order\OrderReference as OrderReferenceResource;
  19. use Temando\Shipping\Model\ResourceModel\Repository\ShipmentReferenceRepositoryInterface;
  20. use Temando\Shipping\Model\ResourceModel\Repository\ShipmentRepositoryInterface;
  21. use Temando\Shipping\Model\Sales\Service\ShipmentService;
  22. use Temando\Shipping\Model\Shipment\PackageInterface;
  23. use Temando\Shipping\Model\Shipment\PackageItemInterface;
  24. use Temando\Shipping\Model\Shipping\Carrier;
  25. use Temando\Shipping\Model\StreamEventInterface;
  26. use Temando\Shipping\Sync\Exception\EventException;
  27. use Temando\Shipping\Sync\Exception\EventProcessorException;
  28. /**
  29. * Temando Shipment Event Processor
  30. *
  31. * @package Temando\Shipping\Sync
  32. * @author Christoph Aßmann <christoph.assmann@netresearch.de>
  33. * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
  34. * @link https://www.temando.com/
  35. */
  36. class ShipmentProcessor implements EntityProcessorInterface
  37. {
  38. /**
  39. * @var OrderRepositoryInterface
  40. */
  41. private $salesOrderRepository;
  42. /**
  43. * @var SalesShipmentRepositoryInterface
  44. */
  45. private $salesShipmentRepository;
  46. /**
  47. * @var ShipmentReferenceRepositoryInterface
  48. */
  49. private $shipmentReferenceRepository;
  50. /**
  51. * @var ShipmentRepositoryInterface
  52. */
  53. private $shipmentRepository;
  54. /**
  55. * @var ShipOrderInterface
  56. */
  57. private $shipOrder;
  58. /**
  59. * @var OrderReferenceResource
  60. */
  61. private $orderReferenceResource;
  62. /**
  63. * @var ShipmentItemCreationInterfaceFactory
  64. */
  65. private $itemCreationFactory;
  66. /**
  67. * @var ShipmentTrackCreationInterfaceFactory
  68. */
  69. private $trackCreationFactory;
  70. /**
  71. * @var ShipmentService
  72. */
  73. private $shipmentService;
  74. /**
  75. * @var ShipmentCreationArgumentsInterfaceFactory
  76. */
  77. private $shipmentCreationArgumentsFactory;
  78. /**
  79. * @var ShipmentCreationArgumentsExtensionInterfaceFactory
  80. */
  81. private $shipmentCreationArgumentsExtensionFactory;
  82. /**
  83. * ShipmentEventProcessor constructor.
  84. * @param OrderRepositoryInterface $salesOrderRepository
  85. * @param SalesShipmentRepositoryInterface $salesShipmentRepository
  86. * @param ShipmentReferenceRepositoryInterface $shipmentReferenceRepository
  87. * @param ShipmentRepositoryInterface $shipmentRepository
  88. * @param ShipOrderInterface $shipOrder
  89. * @param OrderReferenceResource $orderReferenceResource
  90. * @param ShipmentItemCreationInterfaceFactory $itemCreationFactory
  91. * @param ShipmentTrackCreationInterfaceFactory $trackCreationFactory
  92. * @param ShipmentService $shipmentService
  93. * @param ShipmentCreationArgumentsInterfaceFactory $shipmentCreationArgumentsFactory
  94. * @param ShipmentCreationArgumentsExtensionInterfaceFactory $shipmentCreationArgumentsExtensionFactory
  95. */
  96. public function __construct(
  97. OrderRepositoryInterface $salesOrderRepository,
  98. SalesShipmentRepositoryInterface $salesShipmentRepository,
  99. ShipmentReferenceRepositoryInterface $shipmentReferenceRepository,
  100. ShipmentRepositoryInterface $shipmentRepository,
  101. ShipOrderInterface $shipOrder,
  102. OrderReferenceResource $orderReferenceResource,
  103. ShipmentItemCreationInterfaceFactory $itemCreationFactory,
  104. ShipmentTrackCreationInterfaceFactory $trackCreationFactory,
  105. ShipmentService $shipmentService,
  106. ShipmentCreationArgumentsInterfaceFactory $shipmentCreationArgumentsFactory,
  107. ShipmentCreationArgumentsExtensionInterfaceFactory $shipmentCreationArgumentsExtensionFactory
  108. ) {
  109. $this->salesOrderRepository = $salesOrderRepository;
  110. $this->salesShipmentRepository = $salesShipmentRepository;
  111. $this->shipmentReferenceRepository = $shipmentReferenceRepository;
  112. $this->shipmentRepository = $shipmentRepository;
  113. $this->shipOrder = $shipOrder;
  114. $this->orderReferenceResource = $orderReferenceResource;
  115. $this->itemCreationFactory = $itemCreationFactory;
  116. $this->trackCreationFactory = $trackCreationFactory;
  117. $this->shipmentService = $shipmentService;
  118. $this->shipmentCreationArgumentsFactory = $shipmentCreationArgumentsFactory;
  119. $this->shipmentCreationArgumentsExtensionFactory = $shipmentCreationArgumentsExtensionFactory;
  120. }
  121. /**
  122. * @param OrderInterface $salesOrder
  123. * @param string $sku
  124. * @return int|null
  125. */
  126. private function getOrderItemIdBySku(OrderInterface $salesOrder, $sku)
  127. {
  128. foreach ($salesOrder->getItems() as $item) {
  129. if ($item->getSku() === $sku) {
  130. return $item->getItemId();
  131. }
  132. }
  133. return null;
  134. }
  135. /**
  136. * Create new shipment
  137. *
  138. * @param string $extShipmentId
  139. * @return int Processed entity ID.
  140. * @throws EventException
  141. * @throws EventProcessorException
  142. */
  143. private function create(string $extShipmentId): int
  144. {
  145. try {
  146. // load external shipment
  147. $shipment = $this->shipmentRepository->getById($extShipmentId);
  148. } catch (LocalizedException $e) {
  149. throw EventProcessorException::processingFailed(
  150. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  151. $extShipmentId,
  152. $e
  153. );
  154. }
  155. // skip shipment event if no fulfillment with tracking number is available
  156. $fulfillment = $shipment->getFulfillment();
  157. if (!$fulfillment || !$fulfillment->getTrackingReference()) {
  158. throw EventException::operationSkipped(
  159. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  160. StreamEventInterface::EVENT_TYPE_CREATE,
  161. $extShipmentId,
  162. 'No fulfillment information available.'
  163. );
  164. }
  165. // find local order id for external order
  166. $orderId = $this->orderReferenceResource->getOrderIdByExtOrderId($shipment->getOrderId());
  167. if (!$orderId) {
  168. throw EventProcessorException::processingFailed(
  169. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  170. $extShipmentId
  171. );
  172. }
  173. // items
  174. /** @var ShipmentItemCreationInterface[] $creationItems */
  175. $creationItems = [];
  176. $packages = $shipment->getPackages() ?: [];
  177. /** @var PackageItemInterface[] $fulfilledItems */
  178. $fulfilledItems = array_reduce($packages, function (array $items, PackageInterface $package) {
  179. $items = array_merge($items, $package->getItems());
  180. return $items;
  181. }, []);
  182. $salesOrder = $this->salesOrderRepository->get($orderId);
  183. foreach ($fulfilledItems as $fulfilledItem) {
  184. if (!isset($creationItems[$fulfilledItem->getSku()])) {
  185. // add new creation item
  186. $orderItemId = $this->getOrderItemIdBySku($salesOrder, $fulfilledItem->getSku());
  187. $creationItem = $this->itemCreationFactory->create();
  188. $creationItem->setQty($fulfilledItem->getQty());
  189. $creationItem->setOrderItemId($orderItemId);
  190. $creationItems[$fulfilledItem->getSku()] = $creationItem;
  191. } else {
  192. // increase qty of existing creation item
  193. $creationItem = $creationItems[$fulfilledItem->getSku()];
  194. $creationItem->setQty($fulfilledItem->getQty() + $creationItem->getQty());
  195. }
  196. }
  197. // tracking
  198. $tracking = $this->trackCreationFactory->create();
  199. $tracking->setCarrierCode(Carrier::CODE);
  200. $tracking->setTitle($fulfillment->getServiceName());
  201. $tracking->setTrackNumber($fulfillment->getTrackingReference());
  202. // shipping label
  203. /** @var DocumentationInterface $documentation */
  204. $documentation = current($shipment->getDocumentation());
  205. $labelUrl = empty($documentation) ? '' : $documentation->getUrl();
  206. $extensionAttributes = $this->shipmentCreationArgumentsExtensionFactory->create();
  207. $extensionAttributes->setExtLocationId($shipment->getOriginId());
  208. $extensionAttributes->setExtShipmentId($extShipmentId);
  209. $extensionAttributes->setExtTrackingReference($fulfillment->getTrackingReference());
  210. $extensionAttributes->setShippingLabel($labelUrl);
  211. $arguments = $this->shipmentCreationArgumentsFactory->create();
  212. $arguments->setExtensionAttributes($extensionAttributes);
  213. try {
  214. $shipmentId = $this->shipOrder->execute(
  215. $orderId,
  216. $creationItems, // items within partial shipments
  217. true, // Notify the customer (tracking email)
  218. false, // add comment
  219. null,
  220. [$tracking],
  221. [], // Package definition
  222. $arguments
  223. );
  224. } catch (LocalizedException $e) {
  225. throw EventProcessorException::processingFailed(
  226. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  227. $extShipmentId,
  228. $e
  229. );
  230. }
  231. return (int) $shipmentId;
  232. }
  233. /**
  234. * Update existing shipment, i.e. add tracking information, update status
  235. *
  236. * @param string $extShipmentId
  237. * @return int
  238. * @throws EventException
  239. * @throws EventProcessorException
  240. */
  241. private function modify(string $extShipmentId): int
  242. {
  243. try {
  244. $shipmentReference = $this->shipmentReferenceRepository->getByExtShipmentId($extShipmentId);
  245. } catch (NoSuchEntityException $e) {
  246. return $this->create($extShipmentId);
  247. }
  248. try {
  249. // read shipment from platform, trigger post-process operations
  250. $this->shipmentService->read($extShipmentId, (int) $shipmentReference->getShipmentId());
  251. } catch (NoSuchEntityException $exception) {
  252. // shipment does not exist at platform, skip
  253. throw EventException::operationSkipped(
  254. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  255. StreamEventInterface::EVENT_TYPE_MODIFY,
  256. $extShipmentId,
  257. $exception->getMessage()
  258. );
  259. } catch (LocalizedException $exception) {
  260. // processing local shipment failed, try again later
  261. throw EventProcessorException::processingFailed(
  262. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  263. $extShipmentId,
  264. $exception
  265. );
  266. }
  267. return (int) $shipmentReference->getShipmentId();
  268. }
  269. /**
  270. * @param string $operation
  271. * @param string $extShipmentId
  272. * @return int Processed entity ID.
  273. * @throws EventException
  274. * @throws EventProcessorException
  275. */
  276. public function execute(string $operation, string $extShipmentId): int
  277. {
  278. if ($operation == StreamEventInterface::EVENT_TYPE_MODIFY) {
  279. return $this->modify($extShipmentId);
  280. }
  281. if ($operation == StreamEventInterface::EVENT_TYPE_CREATE) {
  282. return $this->create($extShipmentId);
  283. }
  284. throw EventException::unknownOperation(
  285. StreamEventInterface::ENTITY_TYPE_SHIPMENT,
  286. $operation
  287. );
  288. }
  289. }