From 792fc180035ab9c0c30c45838077360bce8ad2ad Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Wed, 7 Sep 2016 15:51:37 +0300 Subject: [PATCH 01/48] MAGETWO-57783: Create and Apply patch for backport to 2.0 --- .../Sales/Api/Data/CommentInterface.php | 8 +- .../CreditmemoCommentCreationInterface.php | 33 ++ .../CreditmemoCreationArgumentsInterface.php | 77 +++ .../Data/CreditmemoItemCreationInterface.php | 32 ++ .../CouldNotRefundExceptionInterface.php | 13 + .../Sales/Api/RefundInvoiceInterface.php | 36 ++ .../Sales/Api/RefundOrderInterface.php | 34 ++ .../Adminhtml/Order/Creditmemo/Save.php | 2 +- .../Exception/CouldNotRefundException.php | 16 + .../Order/Creditmemo/CommentCreation.php | 86 ++++ .../Order/Creditmemo/CreationArguments.php | 104 ++++ .../Order/Creditmemo/CreditmemoValidator.php | 36 ++ .../CreditmemoValidatorInterface.php | 24 + .../Model/Order/Creditmemo/ItemCreation.php | 86 ++++ .../Sales/Model/Order/Creditmemo/Notifier.php | 41 ++ .../Order/Creditmemo/NotifierInterface.php | 31 ++ .../Order/Creditmemo/RefundOperation.php | 125 +++++ .../Order/Creditmemo/Sender/EmailSender.php | 149 ++++++ .../Order/Creditmemo/SenderInterface.php | 29 ++ .../Validation/QuantityValidator.php | 239 +++++++++ .../Creditmemo/Validation/TotalsValidator.php | 52 ++ .../Model/Order/CreditmemoDocumentFactory.php | 149 ++++++ .../Order/Invoice/Validation/CanRefund.php | 106 ++++ .../Sales/Model/Order/PaymentAdapter.php | 20 + .../Model/Order/PaymentAdapterInterface.php | 12 + .../Model/Order/Validation/CanRefund.php | 67 +++ .../Magento/Sales/Model/RefundInvoice.php | 230 +++++++++ app/code/Magento/Sales/Model/RefundOrder.php | 193 ++++++++ .../Order/Creditmemo/Relation/Refund.php | 1 + .../Sales/Model/Service/CreditmemoService.php | 109 ++++- .../Sales/Model/ValidatorInterface.php | 2 +- .../Order/Creditmemo/RefundOperationTest.php | 413 ++++++++++++++++ .../Creditmemo/Sender/EmailSenderTest.php | 361 ++++++++++++++ .../Validation/QuantityValidatorTest.php | 247 ++++++++++ .../Order/CreditmemoDocumentFactoryTest.php | 244 ++++++++++ .../Invoice/Validation/CanRefundTest.php | 131 +++++ .../Unit/Model/Order/PaymentAdapterTest.php | 34 +- .../Model/Order/Validation/CanRefundTest.php | 113 +++++ .../Test/Unit/Model/RefundInvoiceTest.php | 460 ++++++++++++++++++ .../Sales/Test/Unit/Model/RefundOrderTest.php | 414 ++++++++++++++++ .../Model/Service/CreditmemoServiceTest.php | 174 ++++++- app/code/Magento/Sales/etc/di.xml | 15 +- app/code/Magento/Sales/etc/webapi.xml | 12 + .../Sales/Service/V1/RefundOrderTest.php | 314 ++++++++++++ .../order_with_shipping_and_invoice.php | 39 ++ 45 files changed, 5071 insertions(+), 42 deletions(-) create mode 100644 app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php create mode 100644 app/code/Magento/Sales/Api/Data/CreditmemoCreationArgumentsInterface.php create mode 100644 app/code/Magento/Sales/Api/Data/CreditmemoItemCreationInterface.php create mode 100644 app/code/Magento/Sales/Api/Exception/CouldNotRefundExceptionInterface.php create mode 100644 app/code/Magento/Sales/Api/RefundInvoiceInterface.php create mode 100644 app/code/Magento/Sales/Api/RefundOrderInterface.php create mode 100644 app/code/Magento/Sales/Exception/CouldNotRefundException.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/CommentCreation.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/RefundOperation.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php create mode 100644 app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php create mode 100644 app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/CanRefund.php create mode 100644 app/code/Magento/Sales/Model/RefundInvoice.php create mode 100644 app/code/Magento/Sales/Model/RefundOrder.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php create mode 100644 dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php diff --git a/app/code/Magento/Sales/Api/Data/CommentInterface.php b/app/code/Magento/Sales/Api/Data/CommentInterface.php index fcab786319340..6e447e72ab1c2 100644 --- a/app/code/Magento/Sales/Api/Data/CommentInterface.php +++ b/app/code/Magento/Sales/Api/Data/CommentInterface.php @@ -23,14 +23,14 @@ interface CommentInterface const COMMENT = 'comment'; /** - * Gets the comment for the invoice. + * Gets the comment text. * * @return string Comment. */ public function getComment(); /** - * Sets the comment for the invoice. + * Sets the comment text. * * @param string $comment * @return $this @@ -38,14 +38,14 @@ public function getComment(); public function setComment($comment); /** - * Gets the is-visible-on-storefront flag value for the invoice. + * Gets the is-visible-on-storefront flag value for the comment. * * @return int Is-visible-on-storefront flag value. */ public function getIsVisibleOnFront(); /** - * Sets the is-visible-on-storefront flag value for the invoice. + * Sets the is-visible-on-storefront flag value for the comment. * * @param int $isVisibleOnFront * @return $this diff --git a/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php b/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php new file mode 100644 index 0000000000000..283e8600738e7 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/CreditmemoCommentCreationInterface.php @@ -0,0 +1,33 @@ +_objectManager->create( 'Magento\Sales\Api\CreditmemoManagementInterface' ); - $creditmemoManagement->refund($creditmemo, (bool)$data['do_offline'], !empty($data['send_email'])); + $creditmemoManagement->refund($creditmemo, (bool)$data['do_offline']); if (!empty($data['send_email'])) { $this->creditmemoSender->send($creditmemo); diff --git a/app/code/Magento/Sales/Exception/CouldNotRefundException.php b/app/code/Magento/Sales/Exception/CouldNotRefundException.php new file mode 100644 index 0000000000000..59ef4d18b442e --- /dev/null +++ b/app/code/Magento/Sales/Exception/CouldNotRefundException.php @@ -0,0 +1,16 @@ +extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCommentCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } + + /** + * @inheritdoc + */ + public function getComment() + { + return $this->comment; + } + + /** + * @inheritdoc + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * @inheritdoc + */ + public function getIsVisibleOnFront() + { + return $this->isVisibleOnFront; + } + + /** + * @inheritdoc + */ + public function setIsVisibleOnFront($isVisibleOnFront) + { + $this->isVisibleOnFront = $isVisibleOnFront; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php new file mode 100644 index 0000000000000..fd082bb1dd474 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreationArguments.php @@ -0,0 +1,104 @@ +shippingAmount; + } + + /** + * @inheritdoc + */ + public function getAdjustmentPositive() + { + return $this->adjustmentPositive; + } + + /** + * @inheritdoc + */ + public function getAdjustmentNegative() + { + return $this->adjustmentNegative; + } + + /** + * @inheritdoc + */ + public function setShippingAmount($amount) + { + $this->shippingAmount = $amount; + return $this; + } + + /** + * @inheritdoc + */ + public function setAdjustmentPositive($amount) + { + $this->adjustmentPositive = $amount; + return $this; + } + + /** + * @inheritdoc + */ + public function setAdjustmentNegative($amount) + { + $this->adjustmentNegative = $amount; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php new file mode 100644 index 0000000000000..e49a08e32d839 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidator.php @@ -0,0 +1,36 @@ +validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate(CreditmemoInterface $entity, array $validators) + { + return $this->validator->validate($entity, $validators); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php new file mode 100644 index 0000000000000..3889f3b985ff0 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php @@ -0,0 +1,24 @@ +orderItemId; + } + + /** + * {@inheritdoc} + */ + public function setOrderItemId($orderItemId) + { + $this->orderItemId = $orderItemId; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getQty() + { + return $this->qty; + } + + /** + * {@inheritdoc} + */ + public function setQty($qty) + { + $this->qty = $qty; + return $this; + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\CreditmemoItemCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php new file mode 100644 index 0000000000000..47dbecca6b59b --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Notifier.php @@ -0,0 +1,41 @@ +senders = $senders; + } + + /** + * {@inheritdoc} + */ + public function notify( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + foreach ($this->senders as $sender) { + $sender->send($order, $creditmemo, $comment, $forceSyncMode); + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php new file mode 100644 index 0000000000000..ef42bd18633cf --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/NotifierInterface.php @@ -0,0 +1,31 @@ +eventManager = $context->getEventDispatcher(); + $this->priceCurrency = $priceCurrency; + } + + /** + * @param CreditmemoInterface $creditmemo + * @param OrderInterface $order + * @param bool $online + * @return OrderInterface + */ + public function execute(CreditmemoInterface $creditmemo, OrderInterface $order, $online = false) + { + if ($creditmemo->getState() == Creditmemo::STATE_REFUNDED + && $creditmemo->getOrderId() == $order->getEntityId() + ) { + foreach ($creditmemo->getItems() as $item) { + if ($item->isDeleted()) { + continue; + } + $item->setCreditMemo($creditmemo); + if ($item->getQty() > 0) { + $item->register(); + } else { + $item->isDeleted(true); + } + } + + $baseOrderRefund = $this->priceCurrency->round( + $order->getBaseTotalRefunded() + $creditmemo->getBaseGrandTotal() + ); + $orderRefund = $this->priceCurrency->round( + $order->getTotalRefunded() + $creditmemo->getGrandTotal() + ); + $order->setBaseTotalRefunded($baseOrderRefund); + $order->setTotalRefunded($orderRefund); + + $order->setBaseSubtotalRefunded($order->getBaseSubtotalRefunded() + $creditmemo->getBaseSubtotal()); + $order->setSubtotalRefunded($order->getSubtotalRefunded() + $creditmemo->getSubtotal()); + + $order->setBaseTaxRefunded($order->getBaseTaxRefunded() + $creditmemo->getBaseTaxAmount()); + $order->setTaxRefunded($order->getTaxRefunded() + $creditmemo->getTaxAmount()); + $order->setBaseDiscountTaxCompensationRefunded( + $order->getBaseDiscountTaxCompensationRefunded() + $creditmemo->getBaseDiscountTaxCompensationAmount() + ); + $order->setDiscountTaxCompensationRefunded( + $order->getDiscountTaxCompensationRefunded() + $creditmemo->getDiscountTaxCompensationAmount() + ); + + $order->setBaseShippingRefunded($order->getBaseShippingRefunded() + $creditmemo->getBaseShippingAmount()); + $order->setShippingRefunded($order->getShippingRefunded() + $creditmemo->getShippingAmount()); + + $order->setBaseShippingTaxRefunded( + $order->getBaseShippingTaxRefunded() + $creditmemo->getBaseShippingTaxAmount() + ); + $order->setShippingTaxRefunded($order->getShippingTaxRefunded() + $creditmemo->getShippingTaxAmount()); + + $order->setAdjustmentPositive($order->getAdjustmentPositive() + $creditmemo->getAdjustmentPositive()); + $order->setBaseAdjustmentPositive( + $order->getBaseAdjustmentPositive() + $creditmemo->getBaseAdjustmentPositive() + ); + + $order->setAdjustmentNegative($order->getAdjustmentNegative() + $creditmemo->getAdjustmentNegative()); + $order->setBaseAdjustmentNegative( + $order->getBaseAdjustmentNegative() + $creditmemo->getBaseAdjustmentNegative() + ); + + $order->setDiscountRefunded($order->getDiscountRefunded() + $creditmemo->getDiscountAmount()); + $order->setBaseDiscountRefunded($order->getBaseDiscountRefunded() + $creditmemo->getBaseDiscountAmount()); + + if ($online) { + $order->setTotalOnlineRefunded($order->getTotalOnlineRefunded() + $creditmemo->getGrandTotal()); + $order->setBaseTotalOnlineRefunded( + $order->getBaseTotalOnlineRefunded() + $creditmemo->getBaseGrandTotal() + ); + } else { + $order->setTotalOfflineRefunded($order->getTotalOfflineRefunded() + $creditmemo->getGrandTotal()); + $order->setBaseTotalOfflineRefunded( + $order->getBaseTotalOfflineRefunded() + $creditmemo->getBaseGrandTotal() + ); + } + + $order->setBaseTotalInvoicedCost( + $order->getBaseTotalInvoicedCost() - $creditmemo->getBaseCost() + ); + + $creditmemo->setDoTransaction($online); + $order->getPayment()->refund($creditmemo); + + $this->eventManager->dispatch('sales_order_creditmemo_refund', ['creditmemo' => $creditmemo]); + } + + return $order; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php new file mode 100644 index 0000000000000..76210505fd467 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -0,0 +1,149 @@ +paymentHelper = $paymentHelper; + $this->creditmemoResource = $creditmemoResource; + $this->globalConfig = $globalConfig; + $this->eventManager = $eventManager; + } + + /** + * Sends order creditmemo email to the customer. + * + * Email will be sent immediately in two cases: + * + * - if asynchronous email sending is disabled in global settings + * - if $forceSyncMode parameter is set to TRUE + * + * Otherwise, email will be sent later during running of + * corresponding cron job. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return bool + */ + public function send( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + $creditmemo->setSendEmail(true); + + if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $transport = [ + 'order' => $order, + 'creditmemo' => $creditmemo, + 'comment' => $comment ? $comment->getComment() : '', + 'billing' => $order->getBillingAddress(), + 'payment_html' => $this->getPaymentHtml($order), + 'store' => $order->getStore(), + 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + ]; + + $this->eventManager->dispatch( + 'email_creditmemo_set_template_vars_before', + ['sender' => $this, 'transport' => $transport] + ); + + $this->templateContainer->setTemplateVars($transport); + + if ($this->checkAndSend($order)) { + $creditmemo->setEmailSent(true); + + $this->creditmemoResource->saveAttribute($creditmemo, ['send_email', 'email_sent']); + + return true; + } + } else { + $creditmemo->setEmailSent(null); + + $this->creditmemoResource->saveAttribute($creditmemo, 'email_sent'); + } + + $this->creditmemoResource->saveAttribute($creditmemo, 'send_email'); + + return false; + } + + /** + * Returns payment info block as HTML. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return string + */ + private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->paymentHelper->getInfoBlockHtml( + $order->getPayment(), + $this->identityContainer->getStore()->getStoreId() + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php new file mode 100644 index 0000000000000..00d316a8ec98a --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/SenderInterface.php @@ -0,0 +1,29 @@ +orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->priceCurrency = $priceCurrency; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + /** + * @var $entity CreditmemoInterface + */ + if ($entity->getOrderId() === null) { + return [__('Order Id is required for shipment document')]; + } + + $messages = []; + + $order = $this->orderRepository->get($entity->getOrderId()); + $orderItemsById = $this->getOrderItems($order); + $invoiceQtysRefundLimits = $this->getInvoiceQtysRefundLimits($entity, $order); + + $totalQuantity = 0; + foreach ($entity->getItems() as $item) { + if (!isset($orderItemsById[$item->getOrderItemId()])) { + $messages[] = __( + 'The creditmemo contains product SKU "%1" that is not part of the original order.', + $item->getSku() + ); + continue; + } + $orderItem = $orderItemsById[$item->getOrderItemId()]; + + if ( + !$this->canRefundItem($orderItem, $item->getQty(), $invoiceQtysRefundLimits) || + !$this->isQtyAvailable($orderItem, $item->getQty()) + ) { + $messages[] =__( + 'The quantity to creditmemo must not be greater than the unrefunded quantity' + . ' for product SKU "%1".', + $orderItem->getSku() + ); + } else { + $totalQuantity += $item->getQty(); + } + } + + if ($entity->getGrandTotal() <= 0) { + $messages[] = __('The credit memo\'s total must be positive.'); + } elseif ($totalQuantity <= 0 && !$this->canRefundShipping($order)) { + $messages[] = __('You can\'t create a creditmemo without products.'); + } + + return $messages; + } + + /** + * We can have problem with float in php (on some server $a=762.73;$b=762.73; $a-$b!=0) + * for this we have additional diapason for 0 + * TotalPaid - contains amount, that were not rounded. + * + * @param OrderInterface $order + * @return bool + */ + private function canRefundShipping(OrderInterface $order) + { + return !abs($this->priceCurrency->round($order->getShippingAmount()) - $order->getShippingRefunded()) < .0001; + } + + /** + * @param CreditmemoInterface $creditmemo + * @param OrderInterface $order + * @return array + */ + private function getInvoiceQtysRefundLimits(CreditmemoInterface $creditmemo, OrderInterface $order) + { + $invoiceQtysRefundLimits = []; + if ($creditmemo->getInvoiceId() !== null) { + $invoiceQtysRefunded = []; + $invoice = $this->invoiceRepository->get($creditmemo->getInvoiceId()); + foreach ($order->getCreditmemosCollection() as $createdCreditmemo) { + if ( + $createdCreditmemo->getState() != Creditmemo::STATE_CANCELED && + $createdCreditmemo->getInvoiceId() == $invoice->getId() + ) { + foreach ($createdCreditmemo->getAllItems() as $createdCreditmemoItem) { + $orderItemId = $createdCreditmemoItem->getOrderItem()->getId(); + if (isset($invoiceQtysRefunded[$orderItemId])) { + $invoiceQtysRefunded[$orderItemId] += $createdCreditmemoItem->getQty(); + } else { + $invoiceQtysRefunded[$orderItemId] = $createdCreditmemoItem->getQty(); + } + } + } + } + + foreach ($invoice->getItems() as $invoiceItem) { + $invoiceQtyCanBeRefunded = $invoiceItem->getQty(); + $orderItemId = $invoiceItem->getOrderItem()->getId(); + if (isset($invoiceQtysRefunded[$orderItemId])) { + $invoiceQtyCanBeRefunded = $invoiceQtyCanBeRefunded - $invoiceQtysRefunded[$orderItemId]; + } + $invoiceQtysRefundLimits[$orderItemId] = $invoiceQtyCanBeRefunded; + } + } + + return $invoiceQtysRefundLimits; + } + + /** + * @param OrderInterface $order + * @return OrderItemInterface[] + */ + private function getOrderItems(OrderInterface $order) + { + $orderItemsById = []; + foreach ($order->getItems() as $item) { + $orderItemsById[$item->getItemId()] = $item; + } + + return $orderItemsById; + } + + /** + * @param Item $orderItem + * @param int $qty + * @return bool + */ + private function isQtyAvailable(Item $orderItem, $qty) + { + return $qty <= $orderItem->getQtyToRefund() || $orderItem->isDummy(); + } + + /** + * Check if order item can be refunded + * + * @param \Magento\Sales\Model\Order\Item $item + * @param double $qty + * @param array $invoiceQtysRefundLimits + * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function canRefundItem(\Magento\Sales\Model\Order\Item $item, $qty, $invoiceQtysRefundLimits) + { + if ($item->isDummy()) { + if ($item->getHasChildren()) { + foreach ($item->getChildrenItems() as $child) { + if ($qty === null) { + if ($this->canRefundNoDummyItem($child, $invoiceQtysRefundLimits)) { + return true; + } + } else { + if ($qty > 0) { + return true; + } + } + } + return false; + } elseif ($item->getParentItem()) { + $parent = $item->getParentItem(); + if ($qty === null) { + return $this->canRefundNoDummyItem($parent, $invoiceQtysRefundLimits); + } else { + return $qty > 0; + } + } + } else { + return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); + } + } + + /** + * Check if no dummy order item can be refunded + * + * @param \Magento\Sales\Model\Order\Item $item + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundNoDummyItem($item, $invoiceQtysRefundLimits = []) + { + if ($item->getQtyToRefund() < 0) { + return false; + } + if (isset($invoiceQtysRefundLimits[$item->getId()])) { + return $invoiceQtysRefundLimits[$item->getId()] > 0; + } + return true; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php new file mode 100644 index 0000000000000..2cefc377b0674 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/TotalsValidator.php @@ -0,0 +1,52 @@ +priceCurrency = $priceCurrency; + } + + /** + * @inheritDoc + */ + public function validate($entity) + { + $messages = []; + $baseOrderRefund = $this->priceCurrency->round( + $entity->getOrder()->getBaseTotalRefunded() + $entity->getBaseGrandTotal() + ); + if ($baseOrderRefund > $this->priceCurrency->round($entity->getOrder()->getBaseTotalPaid())) { + $baseAvailableRefund = $entity->getOrder()->getBaseTotalPaid() + - $entity->getOrder()->getBaseTotalRefunded(); + + $messages[] = __( + 'The most money available to refund is %1.', + $baseAvailableRefund + ); + } + + return $messages; + } +} diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php new file mode 100644 index 0000000000000..067e3d782a88d --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php @@ -0,0 +1,149 @@ +creditmemoFactory = $creditmemoFactory; + $this->commentFactory = $commentFactory; + $this->orderRepository = $orderRepository; + } + + /** + * Get array with original data for new Creditmemo document + * + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return array + */ + private function getCreditmemoCreationData( + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = ['qtys' => []]; + foreach ($items as $item) { + $data['qtys'][$item->getOrderItemId()] = $item->getQty(); + } + if ($arguments) { + + $data = array_merge( + [ + 'shipping_amount' => $arguments->getShippingAmount(), + 'adjustment_positive' => $arguments->getAdjustmentPositive(), + 'adjustment_negative' => $arguments->getAdjustmentNegative(), + ], + $data + ); + } + return $data; + } + + /** + * Attach comment to the Creditmemo document. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment + * @param bool $appendComment + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + private function attachComment( + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment, + $appendComment = false + ) { + $commentData = [ + 'comment' => $comment->getComment(), + 'is_visible_on_frontend' => $comment->getIsVisibleOnFront() + ]; + $comment = $this->commentFactory->create(['data' => $commentData]); + $comment->setParentId($creditmemo->getEntityId()) + ->setStoreId($creditmemo->getStoreId()) + ->setCreditmemo($creditmemo) + ->setIsCustomerNotified($appendComment); + $creditmemo->setComments([$comment]); + return $creditmemo; + + } + + /** + * Create new Creditmemo + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + public function createFromOrder( + \Magento\Sales\Api\Data\OrderInterface $order, + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = $this->getCreditmemoCreationData($items, $arguments); + $creditmemo = $this->creditmemoFactory->createByOrder($order, $data); + if ($comment) { + $creditmemo = $this->attachComment($creditmemo, $comment, $appendComment); + } + return $creditmemo; + } + + /** + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Api\Data\CreditmemoInterface + */ + public function createFromInvoice( + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + array $items = [], + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $data = $this->getCreditmemoCreationData($items, $arguments); + /** @var $invoice \Magento\Sales\Model\Order\Invoice */ + $invoice->setOrder($this->orderRepository->get($invoice->getOrderId())); + $creditmemo = $this->creditmemoFactory->createByInvoice($invoice, $data); + if ($comment) { + $creditmemo = $this->attachComment($creditmemo, $comment, $appendComment); + } + return $creditmemo; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php b/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php new file mode 100644 index 0000000000000..8e68cade3caa7 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/Validation/CanRefund.php @@ -0,0 +1,106 @@ +paymentRepository = $paymentRepository; + $this->orderRepository = $orderRepository; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + if ( + $entity->getState() == Invoice::STATE_PAID && + $this->isGrandTotalEnoughToRefund($entity) && + $this->isPaymentAllowRefund($entity) + ) { + return []; + } + + return [__('We can\'t create creditmemo for the invoice.')]; + } + + /** + * @param InvoiceInterface $invoice + * @return bool + */ + private function isPaymentAllowRefund(InvoiceInterface $invoice) + { + $order = $this->orderRepository->get($invoice->getOrderId()); + $payment = $order->getPayment(); + if (!$payment instanceof InfoInterface) { + return false; + } + $method = $payment->getMethodInstance(); + return $this->canPartialRefund($method, $payment) || $this->canFullRefund($invoice, $method); + } + + /** + * @param InvoiceInterface $entity + * @return bool + */ + private function isGrandTotalEnoughToRefund(InvoiceInterface $entity) + { + return abs($entity->getBaseGrandTotal() - $entity->getBaseTotalRefunded()) >= .0001; + } + + /** + * @param MethodInterface $method + * @param InfoInterface $payment + * @return bool + */ + private function canPartialRefund(MethodInterface $method, InfoInterface $payment) + { + return $method->canRefund() && + $method->canRefundPartialPerInvoice() && + $payment->getAmountPaid() > $payment->getAmountRefunded(); + } + + /** + * @param InvoiceInterface $invoice + * @param MethodInterface $method + * @return bool + */ + private function canFullRefund(InvoiceInterface $invoice, MethodInterface $method) + { + return $method->canRefund() && !$invoice->getIsUsedForRefund(); + } +} diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php index 84eb0fa07553c..d176ce0566bb3 100644 --- a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php @@ -12,20 +12,40 @@ */ class PaymentAdapter implements PaymentAdapterInterface { + /** + * @var \Magento\Sales\Model\Order\Creditmemo\RefundOperation + */ + private $refundOperation; + /** * @var \Magento\Sales\Model\Order\Invoice\PayOperation */ private $payOperation; /** + * PaymentAdapter constructor. + * @param \Magento\Sales\Model\Order\Creditmemo\RefundOperation $refundOperation * @param \Magento\Sales\Model\Order\Invoice\PayOperation $payOperation */ public function __construct( + \Magento\Sales\Model\Order\Creditmemo\RefundOperation $refundOperation, \Magento\Sales\Model\Order\Invoice\PayOperation $payOperation ) { + $this->refundOperation = $refundOperation; $this->payOperation = $payOperation; } + /** + * {@inheritdoc} + */ + public function refund( + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\OrderInterface $order, + $isOnline = false + ) { + return $this->refundOperation->execute($creditmemo, $order, $isOnline); + } + /** * {@inheritdoc} */ diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php index 0e4b193169da8..3636bc2592f3b 100644 --- a/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php @@ -23,4 +23,16 @@ interface PaymentAdapterInterface * @return OrderInterface */ public function pay(OrderInterface $order, InvoiceInterface $invoice, $capture); + + /** + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param bool $isOnline + * @return \Magento\Sales\Api\Data\OrderInterface + */ + public function refund( + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\OrderInterface $order, + $isOnline = false + ); } diff --git a/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php b/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php new file mode 100644 index 0000000000000..c6fc1a0d705e8 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/CanRefund.php @@ -0,0 +1,67 @@ +priceCurrency = $priceCurrency; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + $messages = []; + if ($entity->getState() === Order::STATE_PAYMENT_REVIEW || + $entity->getState() === Order::STATE_HOLDED || + $entity->getState() === Order::STATE_CANCELED || + $entity->getState() === Order::STATE_CLOSED + ) { + $messages[] = __( + 'A creditmemo can not be created when an order has a status of %1', + $entity->getStatus() + ); + } elseif (!$this->isTotalPaidEnoughForRefund($entity)) { + $messages[] = __('The order does not allow a creditmemo to be created.'); + } + + return $messages; + } + + /** + * We can have problem with float in php (on some server $a=762.73;$b=762.73; $a-$b!=0) + * for this we have additional diapason for 0 + * TotalPaid - contains amount, that were not rounded. + * + * @param OrderInterface $order + * @return bool + */ + private function isTotalPaidEnoughForRefund(OrderInterface $order) + { + return !abs($this->priceCurrency->round($order->getTotalPaid()) - $order->getTotalRefunded()) < .0001; + } +} diff --git a/app/code/Magento/Sales/Model/RefundInvoice.php b/app/code/Magento/Sales/Model/RefundInvoice.php new file mode 100644 index 0000000000000..18837315ef83a --- /dev/null +++ b/app/code/Magento/Sales/Model/RefundInvoice.php @@ -0,0 +1,230 @@ +resourceConnection = $resourceConnection; + $this->orderStateResolver = $orderStateResolver; + $this->orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->creditmemoRepository = $creditmemoRepository; + $this->paymentAdapter = $paymentAdapter; + $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; + $this->notifier = $notifier; + $this->config = $config; + $this->logger = $logger; + $this->invoiceValidator = $invoiceValidator; + } + + /** + * @inheritdoc + */ + public function execute( + $invoiceId, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $invoice = $this->invoiceRepository->get($invoiceId); + $order = $this->orderRepository->get($invoice->getOrderId()); + $creditmemo = $this->creditmemoDocumentFactory->createFromInvoice( + $invoice, + $items, + $comment, + ($appendComment && $notify), + $arguments + ); + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $invoiceValidationResult = $this->invoiceValidator->validate( + $invoice, + [ + \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + $validationMessages = array_merge( + $orderValidationResult, + $invoiceValidationResult, + $creditmemoValidationResult + ); + if (!empty($validationMessages )) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + ); + } + $connection->beginTransaction(); + try { + $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order = $this->paymentAdapter->refund($creditmemo, $order, $isOnline); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, []) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + if (!$isOnline) { + $invoice->setIsUsedForRefund(true); + $invoice->setBaseTotalRefunded( + $invoice->getBaseTotalRefunded() + $creditmemo->getBaseGrandTotal() + ); + } + $this->invoiceRepository->save($invoice); + $order = $this->orderRepository->save($order); + $creditmemo = $this->creditmemoRepository->save($creditmemo); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotRefundException( + __('Could not save a Creditmemo, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifier->notify($order, $creditmemo, $comment); + } + + return $creditmemo->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php new file mode 100644 index 0000000000000..70fc7be4310e6 --- /dev/null +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -0,0 +1,193 @@ +resourceConnection = $resourceConnection; + $this->orderStateResolver = $orderStateResolver; + $this->orderRepository = $orderRepository; + $this->orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->creditmemoRepository = $creditmemoRepository; + $this->paymentAdapter = $paymentAdapter; + $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; + $this->notifier = $notifier; + $this->config = $config; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function execute( + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $order = $this->orderRepository->get($orderId); + $creditmemo = $this->creditmemoDocumentFactory->createFromOrder( + $order, + $items, + $comment, + ($appendComment && $notify), + $arguments + ); + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + $validationMessages = array_merge($orderValidationResult, $creditmemoValidationResult); + if (!empty($validationMessages)) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + ); + } + $connection->beginTransaction(); + try { + $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order = $this->paymentAdapter->refund($creditmemo, $order); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, []) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + + $order = $this->orderRepository->save($order); + $creditmemo = $this->creditmemoRepository->save($creditmemo); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotRefundException( + __('Could not save a Creditmemo, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifier->notify($order, $creditmemo, $comment); + } + + return $creditmemo->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Relation/Refund.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Relation/Refund.php index 6c38259432780..82df0aa0bebdf 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Relation/Refund.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Relation/Refund.php @@ -10,6 +10,7 @@ /** * Class Relation + * @deprecated */ class Refund implements RelationInterface { diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index 41ebcd57b966d..4889ccd3750d5 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -8,6 +8,7 @@ /** * Class CreditmemoService + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CreditmemoService implements \Magento\Sales\Api\CreditmemoManagementInterface { @@ -46,6 +47,26 @@ class CreditmemoService implements \Magento\Sales\Api\CreditmemoManagementInterf */ protected $eventManager; + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var \Magento\Sales\Model\Order\PaymentAdapterInterface + */ + private $paymentAdapter; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var \Magento\Sales\Api\InvoiceRepositoryInterface + */ + private $invoiceRepository; + /** * @param \Magento\Sales\Api\CreditmemoRepositoryInterface $creditmemoRepository * @param \Magento\Sales\Api\CreditmemoCommentRepositoryInterface $creditmemoCommentRepository @@ -130,6 +151,7 @@ public function notify($id) * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo * @param bool $offlineRequested * @return \Magento\Sales\Api\Data\CreditmemoInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function refund( \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, @@ -138,18 +160,31 @@ public function refund( $this->validateForRefund($creditmemo); $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); - foreach ($creditmemo->getAllItems() as $item) { - if ($item->getQty() > 0) { - $item->register(); - } else { - $item->isDeleted(true); + $connection = $this->getResource()->getConnection('sales'); + $connection->beginTransaction(); + try { + $order = $this->getPaymentAdapter()->refund( + $creditmemo, + $creditmemo->getOrder(), + !$offlineRequested + ); + $this->getOrderRepository()->save($order); + $invoice = $creditmemo->getInvoice(); + if ($invoice && !$offlineRequested) { + $invoice->setIsUsedForRefund(true); + $invoice->setBaseTotalRefunded( + $invoice->getBaseTotalRefunded() + $creditmemo->getBaseGrandTotal() + ); + $creditmemo->setInvoiceId($invoice->getId()); + $this->getInvoiceRepository()->save($creditmemo->getInvoice()); } + $this->creditmemoRepository->save($creditmemo); + $connection->commit(); + } catch (\Exception $e) { + $connection->rollBack(); + throw new \Magento\Framework\Exception\LocalizedException($e->getMessage()); } - $creditmemo->setDoTransaction(!$offlineRequested); - - $this->eventManager->dispatch('sales_order_creditmemo_refund', ['creditmemo' => $creditmemo]); - $this->creditmemoRepository->save($creditmemo); return $creditmemo; } @@ -182,4 +217,60 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface } return true; } + + /** + * @return \Magento\Sales\Model\Order\PaymentAdapterInterface + * + * @deprecated + */ + private function getPaymentAdapter() + { + if ($this->paymentAdapter === null) { + $this->paymentAdapter = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\Order\PaymentAdapterInterface::class); + } + return $this->paymentAdapter; + } + + /** + * @return \Magento\Framework\App\ResourceConnection|mixed + * + * @deprecated + */ + private function getResource() + { + if ($this->resource === null) { + $this->resource = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\App\ResourceConnection::class); + } + return $this->resource; + } + + /** + * @return \Magento\Sales\Api\OrderRepositoryInterface + * + * @deprecated + */ + private function getOrderRepository() + { + if ($this->orderRepository === null) { + $this->orderRepository = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Api\OrderRepositoryInterface::class); + } + return $this->orderRepository; + } + + /** + * @return \Magento\Sales\Api\InvoiceRepositoryInterface + * + * @deprecated + */ + private function getInvoiceRepository() + { + if ($this->invoiceRepository === null) { + $this->invoiceRepository = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Api\InvoiceRepositoryInterface::class); + } + return $this->invoiceRepository; + } } diff --git a/app/code/Magento/Sales/Model/ValidatorInterface.php b/app/code/Magento/Sales/Model/ValidatorInterface.php index 1882782e314f7..4489af44f4036 100644 --- a/app/code/Magento/Sales/Model/ValidatorInterface.php +++ b/app/code/Magento/Sales/Model/ValidatorInterface.php @@ -15,7 +15,7 @@ interface ValidatorInterface { /** * @param object $entity - * @return array + * @return \Magento\Framework\Phrase[] * @throws DocumentValidationException * @throws NoSuchEntityException */ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php new file mode 100644 index 0000000000000..7589581f6ee2c --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php @@ -0,0 +1,413 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseCost', 'setDoTransaction']) + ->getMockForAbstractClass(); + + $this->paymentMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->setMethods(['refund']) + ->getMockForAbstractClass(); + + $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->setMethods(['round']) + ->getMockForAbstractClass(); + + $contextMock = $this->getMockBuilder(\Magento\Framework\Model\Context::class) + ->disableOriginalConstructor() + ->setMethods(['getEventDispatcher']) + ->getMock(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $contextMock->expects($this->once()) + ->method('getEventDispatcher') + ->willReturn($this->eventManagerMock); + + $this->subject = new \Magento\Sales\Model\Order\Creditmemo\RefundOperation( + $contextMock, + $this->priceCurrencyMock + ); + } + + /** + * @param string $state + * @dataProvider executeNotRefundedCreditmemoDataProvider + */ + public function testExecuteNotRefundedCreditmemo($state) + { + $this->creditmemoMock->expects($this->once()) + ->method('getState') + ->willReturn($state); + $this->orderMock->expects($this->never()) + ->method('getEntityId'); + $this->assertEquals( + $this->orderMock, + $this->subject->execute( + $this->creditmemoMock, + $this->orderMock + ) + ); + } + + /** + * Data provider for testExecuteNotRefundedCreditmemo + * @return array + */ + public function executeNotRefundedCreditmemoDataProvider() + { + return [ + [Creditmemo::STATE_OPEN], + [Creditmemo::STATE_CANCELED], + ]; + } + + public function testExecuteWithWrongOrder() + { + $creditmemoOrderId = 1; + $orderId = 2; + $this->creditmemoMock->expects($this->once()) + ->method('getState') + ->willReturn(Creditmemo::STATE_REFUNDED); + $this->creditmemoMock->expects($this->once()) + ->method('getOrderId') + ->willReturn($creditmemoOrderId); + $this->orderMock->expects($this->once()) + ->method('getEntityId') + ->willReturn($orderId); + $this->orderMock->expects($this->never()) + ->method('setTotalRefunded'); + $this->assertEquals( + $this->orderMock, + $this->subject->execute($this->creditmemoMock, $this->orderMock) + ); + } + + /** + * @param array $amounts + * @dataProvider baseAmountsDataProvider + */ + public function testExecuteOffline($amounts) + { + $orderId = 1; + $online = false; + $this->creditmemoMock->expects($this->once()) + ->method('getState') + ->willReturn(Creditmemo::STATE_REFUNDED); + $this->creditmemoMock->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + $this->orderMock->expects($this->once()) + ->method('getEntityId') + ->willReturn($orderId); + + $this->registerItems(); + + $this->priceCurrencyMock->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + + $this->setBaseAmounts($amounts); + $this->orderMock->expects($this->once()) + ->method('setTotalOfflineRefunded') + ->with(2); + $this->orderMock->expects($this->once()) + ->method('getTotalOfflineRefunded') + ->willReturn(0); + $this->orderMock->expects($this->once()) + ->method('setBaseTotalOfflineRefunded') + ->with(1); + $this->orderMock->expects($this->once()) + ->method('getBaseTotalOfflineRefunded') + ->willReturn(0); + $this->orderMock->expects($this->never()) + ->method('setTotalOnlineRefunded'); + + $this->orderMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + + $this->paymentMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock); + + $this->creditmemoMock->expects($this->once()) + ->method('setDoTransaction') + ->with($online); + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'sales_order_creditmemo_refund', + ['creditmemo' => $this->creditmemoMock] + ); + + $this->assertEquals( + $this->orderMock, + $this->subject->execute($this->creditmemoMock, $this->orderMock, $online) + ); + } + + /** + * @param array $amounts + * @dataProvider baseAmountsDataProvider + */ + public function testExecuteOnline($amounts) + { + $orderId = 1; + $online = true; + $this->creditmemoMock->expects($this->once()) + ->method('getState') + ->willReturn(Creditmemo::STATE_REFUNDED); + $this->creditmemoMock->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + $this->orderMock->expects($this->once()) + ->method('getEntityId') + ->willReturn($orderId); + + $this->registerItems(); + + $this->priceCurrencyMock->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + + $this->setBaseAmounts($amounts); + $this->orderMock->expects($this->once()) + ->method('setTotalOnlineRefunded') + ->with(2); + $this->orderMock->expects($this->once()) + ->method('getTotalOnlineRefunded') + ->willReturn(0); + $this->orderMock->expects($this->once()) + ->method('setBaseTotalOnlineRefunded') + ->with(1); + $this->orderMock->expects($this->once()) + ->method('getBaseTotalOnlineRefunded') + ->willReturn(0); + $this->orderMock->expects($this->never()) + ->method('setTotalOfflineRefunded'); + + $this->creditmemoMock->expects($this->once()) + ->method('setDoTransaction') + ->with($online); + + $this->orderMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + $this->paymentMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock); + + $this->assertEquals( + $this->orderMock, + $this->subject->execute($this->creditmemoMock, $this->orderMock, $online) + ); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function baseAmountsDataProvider() + { + return [ + [[ + 'setBaseTotalRefunded' => [ + 'result' => 2, + 'order' => ['method' => 'getBaseTotalRefunded', 'amount' => 1], + 'creditmemo' => ['method' => 'getBaseGrandTotal', 'amount' => 1] + ], + 'setTotalRefunded' => [ + 'result' => 4, + 'order' => ['method' => 'getTotalRefunded', 'amount' => 2], + 'creditmemo' => ['method' => 'getGrandTotal', 'amount' => 2] + ], + 'setBaseSubtotalRefunded' => [ + 'result' => 6, + 'order' => ['method' => 'getBaseSubtotalRefunded', 'amount' => 3], + 'creditmemo' => ['method' => 'getBaseSubtotal', 'amount' => 3] + ], + 'setSubtotalRefunded' => [ + 'result' => 6, + 'order' => ['method' => 'getSubtotalRefunded', 'amount' => 3], + 'creditmemo' => ['method' => 'getSubtotal', 'amount' => 3] + ], + 'setBaseTaxRefunded' => [ + 'result' => 8, + 'order' => ['method' => 'getBaseTaxRefunded', 'amount' => 4], + 'creditmemo' => ['method' => 'getBaseTaxAmount', 'amount' => 4] + ], + 'setTaxRefunded' => [ + 'result' => 10, + 'order' => ['method' => 'getTaxRefunded', 'amount' => 5], + 'creditmemo' => ['method' => 'getTaxAmount', 'amount' => 5] + ], + 'setBaseDiscountTaxCompensationRefunded' => [ + 'result' => 12, + 'order' => ['method' => 'getBaseDiscountTaxCompensationRefunded', 'amount' => 6], + 'creditmemo' => ['method' => 'getBaseDiscountTaxCompensationAmount', 'amount' => 6] + ], + 'setDiscountTaxCompensationRefunded' => [ + 'result' => 14, + 'order' => ['method' => 'getDiscountTaxCompensationRefunded', 'amount' => 7], + 'creditmemo' => ['method' => 'getDiscountTaxCompensationAmount', 'amount' => 7] + ], + 'setBaseShippingRefunded' => [ + 'result' => 16, + 'order' => ['method' => 'getBaseShippingRefunded', 'amount' => 8], + 'creditmemo' => ['method' => 'getBaseShippingAmount', 'amount' => 8] + ], + 'setShippingRefunded' => [ + 'result' => 18, + 'order' => ['method' => 'getShippingRefunded', 'amount' => 9], + 'creditmemo' => ['method' => 'getShippingAmount', 'amount' => 9] + ], + 'setBaseShippingTaxRefunded' => [ + 'result' => 20, + 'order' => ['method' => 'getBaseShippingTaxRefunded', 'amount' => 10], + 'creditmemo' => ['method' => 'getBaseShippingTaxAmount', 'amount' => 10] + ], + 'setShippingTaxRefunded' => [ + 'result' => 22, + 'order' => ['method' => 'getShippingTaxRefunded', 'amount' => 11], + 'creditmemo' => ['method' => 'getShippingTaxAmount', 'amount' => 11] + ], + 'setAdjustmentPositive' => [ + 'result' => 24, + 'order' => ['method' => 'getAdjustmentPositive', 'amount' => 12], + 'creditmemo' => ['method' => 'getAdjustmentPositive', 'amount' => 12] + ], + 'setBaseAdjustmentPositive' => [ + 'result' => 26, + 'order' => ['method' => 'getBaseAdjustmentPositive', 'amount' => 13], + 'creditmemo' => ['method' => 'getBaseAdjustmentPositive', 'amount' => 13] + ], + 'setAdjustmentNegative' => [ + 'result' => 28, + 'order' => ['method' => 'getAdjustmentNegative', 'amount' => 14], + 'creditmemo' => ['method' => 'getAdjustmentNegative', 'amount' => 14] + ], + 'setBaseAdjustmentNegative' => [ + 'result' => 30, + 'order' => ['method' => 'getBaseAdjustmentNegative', 'amount' => 15], + 'creditmemo' => ['method' => 'getBaseAdjustmentNegative', 'amount' => 15] + ], + 'setDiscountRefunded' => [ + 'result' => 32, + 'order' => ['method' => 'getDiscountRefunded', 'amount' => 16], + 'creditmemo' => ['method' => 'getDiscountAmount', 'amount' => 16] + ], + 'setBaseDiscountRefunded' => [ + 'result' => 34, + 'order' => ['method' => 'getBaseDiscountRefunded', 'amount' => 17], + 'creditmemo' => ['method' => 'getBaseDiscountAmount', 'amount' => 17] + ], + 'setBaseTotalInvoicedCost' => [ + 'result' => 7, + 'order' => ['method' => 'getBaseTotalInvoicedCost', 'amount' => 18], + 'creditmemo' => ['method' => 'getBaseCost', 'amount' => 11] + ], + ]], + ]; + } + + private function setBaseAmounts($amounts) + { + foreach ($amounts as $amountName => $summands) { + $this->orderMock->expects($this->once()) + ->method($amountName) + ->with($summands['result']); + $this->orderMock->expects($this->once()) + ->method($summands['order']['method']) + ->willReturn($summands['order']['amount']); + $this->creditmemoMock->expects($this->any()) + ->method($summands['creditmemo']['method']) + ->willReturn($summands['creditmemo']['amount']); + } + } + + private function registerItems() + { + $item1 = $this->getCreditmemoItemMock(); + $item1->expects($this->once())->method('isDeleted')->willReturn(true); + $item1->expects($this->never())->method('setCreditMemo'); + + $item2 = $this->getCreditmemoItemMock(); + $item2->expects($this->at(0))->method('isDeleted')->willReturn(false); + $item2->expects($this->once())->method('setCreditMemo')->with($this->creditmemoMock); + $item2->expects($this->once())->method('getQty')->willReturn(0); + $item2->expects($this->at(3))->method('isDeleted')->with(true); + $item2->expects($this->never())->method('register'); + + $item3 = $this->getCreditmemoItemMock(); + $item3->expects($this->once())->method('isDeleted')->willReturn(false); + $item3->expects($this->once())->method('setCreditMemo')->with($this->creditmemoMock); + $item3->expects($this->once())->method('getQty')->willReturn(1); + $item3->expects($this->once())->method('register'); + + $this->creditmemoMock->expects($this->any()) + ->method('getItems') + ->willReturn([$item1, $item2, $item3]); + } + + private function getCreditmemoItemMock() + { + return $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isDeleted', 'setCreditMemo', 'getQty', 'register']) + ->getMockForAbstractClass(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php new file mode 100644 index 0000000000000..d1fe28b21b59b --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -0,0 +1,361 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock->expects($this->any()) + ->method('getStoreId') + ->willReturn(1); + $this->orderMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Sender::class) + ->disableOriginalConstructor() + ->setMethods(['send', 'sendCopyTo']) + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Creditmemo::class) + ->disableOriginalConstructor() + ->setMethods(['setSendEmail', 'setEmailSent']) + ->getMock(); + + $this->commentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->commentMock->expects($this->any()) + ->method('getComment') + ->willReturn('Comment text'); + + $this->addressMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->any()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + + $this->globalConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentInfoMock = $this->getMockBuilder(\Magento\Payment\Model\Info::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($this->paymentInfoMock); + + $this->paymentHelperMock = $this->getMockBuilder(\Magento\Payment\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentHelperMock->expects($this->any()) + ->method('getInfoBlockHtml') + ->with($this->paymentInfoMock, 1) + ->willReturn('Payment Info Block'); + + $this->creditmemoResourceMock = $this->getMockBuilder( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo::class + )->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address\Renderer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->with($this->addressMock, 'html') + ->willReturn('Formatted address'); + + $this->templateContainerMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Container\Template::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderBuilderFactoryMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\SenderBuilderFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->subject = new \Magento\Sales\Model\Order\Creditmemo\Sender\EmailSender( + $this->templateContainerMock, + $this->identityContainerMock, + $this->senderBuilderFactoryMock, + $this->loggerMock, + $this->addressRendererMock, + $this->paymentHelperMock, + $this->creditmemoResourceMock, + $this->globalConfigMock, + $this->eventManagerMock + ); + } + + /** + * @param int $configValue + * @param bool $forceSyncMode + * @param bool $isComment + * @param bool $emailSendingResult + * + * @dataProvider sendDataProvider + * + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult) + { + $this->globalConfigMock->expects($this->once()) + ->method('getValue') + ->with('sales_email/general/async_sending') + ->willReturn($configValue); + + if (!$isComment) { + $this->commentMock = null; + } + + $this->creditmemoMock->expects($this->once()) + ->method('setSendEmail') + ->with(true); + + if (!$configValue || $forceSyncMode) { + $transport = [ + 'order' => $this->orderMock, + 'creditmemo' => $this->creditmemoMock, + 'comment' => $isComment ? 'Comment text' : '', + 'billing' => $this->addressMock, + 'payment_html' => 'Payment Info Block', + 'store' => $this->storeMock, + 'formattedShippingAddress' => 'Formatted address', + 'formattedBillingAddress' => 'Formatted address', + ]; + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'email_creditmemo_set_template_vars_before', + [ + 'sender' => $this->subject, + 'transport' => $transport, + ] + ); + + $this->templateContainerMock->expects($this->once()) + ->method('setTemplateVars') + ->with($transport); + + $this->identityContainerMock->expects($this->once()) + ->method('isEnabled') + ->willReturn($emailSendingResult); + + if ($emailSendingResult) { + $this->senderBuilderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->senderMock); + + $this->senderMock->expects($this->once()) + ->method('send'); + + $this->senderMock->expects($this->once()) + ->method('sendCopyTo'); + + $this->creditmemoMock->expects($this->once()) + ->method('setEmailSent') + ->with(true); + + $this->creditmemoResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->creditmemoMock, ['send_email', 'email_sent']); + + $this->assertTrue( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } else { + $this->creditmemoResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } else { + $this->creditmemoMock->expects($this->once()) + ->method('setEmailSent') + ->with(null); + + $this->creditmemoResourceMock->expects($this->at(0)) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'email_sent'); + $this->creditmemoResourceMock->expects($this->at(1)) + ->method('saveAttribute') + ->with($this->creditmemoMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->creditmemoMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } + + /** + * @return array + */ + public function sendDataProvider() + { + return [ + 'Successful sync sending with comment' => [0, false, true, true], + 'Successful sync sending without comment' => [0, false, false, true], + 'Failed sync sending with comment' => [0, false, true, false], + 'Successful forced sync sending with comment' => [1, true, true, true], + 'Async sending' => [1, false, false, false], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php new file mode 100644 index 0000000000000..5c3fa75c97af1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php @@ -0,0 +1,247 @@ +orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->priceCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->validator = new QuantityValidator( + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->priceCurrencyMock + ); + } + + public function testValidateWithoutItems() + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn(1); + $creditmemoMock->expects($this->once())->method('getItems') + ->willReturn([]); + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([]); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with(1) + ->willReturn($orderMock); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn(0); + $this->assertEquals( + [ + __('The credit memo\'s total must be positive.') + ], + $this->validator->validate($creditmemoMock) + ); + } + + public function testValidateWithoutOrder() + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->once())->method('getOrderId') + ->willReturn(null); + $creditmemoMock->expects($this->never())->method('getItems'); + $this->assertEquals( + [__('Order Id is required for shipment document')], + $this->validator->validate($creditmemoMock) + ); + } + + public function testValidateWithWrongItemId() + { + $orderId = 1; + $orderItemId = 1; + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn($orderId); + $creditmemoItemMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoItemInterface::class + )->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoItemMock->expects($this->once())->method('getOrderItemId') + ->willReturn($orderItemId); + $creditmemoItemSku = 'sku'; + $creditmemoItemMock->expects($this->once())->method('getSku') + ->willReturn($creditmemoItemSku); + $creditmemoMock->expects($this->exactly(1))->method('getItems') + ->willReturn([$creditmemoItemMock]); + + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([]); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn(12); + + $this->assertEquals( + [ + __( + 'The creditmemo contains product SKU "%1" that is not part of the original order.', + $creditmemoItemSku + ), + __('You can\'t create a creditmemo without products.') + ], + $this->validator->validate($creditmemoMock) + ); + } + + /** + * @param int $orderId + * @param int $orderItemId + * @param int $qtyToRequest + * @param int $qtyToRefund + * @param string $sku + * @param array $expected + * @dataProvider dataProviderForValidateQty + */ + public function testValidate($orderId, $orderItemId, $qtyToRequest, $qtyToRefund, $sku, $total, array $expected) + { + $creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoMock->expects($this->exactly(2))->method('getOrderId') + ->willReturn($orderId); + $creditmemoMock->expects($this->once())->method('getGrandTotal') + ->willReturn($total); + $creditmemoItemMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoItemInterface::class + )->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditmemoItemMock->expects($this->exactly(2))->method('getOrderItemId') + ->willReturn($orderItemId); + $creditmemoItemMock->expects($this->never())->method('getSku') + ->willReturn($sku); + $creditmemoItemMock->expects($this->atLeastOnce())->method('getQty') + ->willReturn($qtyToRequest); + $creditmemoMock->expects($this->exactly(1))->method('getItems') + ->willReturn([$creditmemoItemMock]); + + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $orderItemMock->expects($this->exactly(2))->method('getQtyToRefund') + ->willReturn($qtyToRefund); + $creditmemoItemMock->expects($this->any())->method('getQty') + ->willReturn($qtyToRequest); + $orderMock->expects($this->once())->method('getItems') + ->willReturn([$orderItemMock]); + $orderItemMock->expects($this->once())->method('getItemId') + ->willReturn($orderItemId); + $orderItemMock->expects($this->any())->method('getSku') + ->willReturn($sku); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + + $this->assertEquals( + $expected, + $this->validator->validate($creditmemoMock) + ); + } + + /** + * @return array + */ + public function dataProviderForValidateQty() + { + $sku = 'sku'; + + return [ + [ + 'orderId' => 1, + 'orderItemId' => 1, + 'qtyToRequest' => 1, + 'qtyToRefund' => 1, + 'sku', + 'total' => 15, + 'expected' => [] + ], + [ + 'orderId' => 1, + 'orderItemId' => 1, + 'qtyToRequest' => 2, + 'qtyToRefund' => 1, + 'sku', + 'total' => 0, + 'expected' => [ + __( + 'The quantity to creditmemo must not be greater than the unrefunded quantity' + . ' for product SKU "%1".', + $sku + ), + __('The credit memo\'s total must be positive.') + ] + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php new file mode 100644 index 0000000000000..ab11290d37219 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php @@ -0,0 +1,244 @@ +objectManager = new ObjectManager($this); + $this->creditmemoFactoryMock = $this->getMockBuilder(CreditmemoFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentFactoryMock = + $this->getMockBuilder('Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory') + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceMock = $this->getMockBuilder(Invoice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(11); + + $this->commentMock = $this->getMockBuilder(CreditmemoCommentInterface::class) + ->disableOriginalConstructor() + ->setMethods( + array_merge( + get_class_methods(CreditmemoCommentInterface::class), + ['setStoreId', 'setCreditmemo'] + ) + ) + ->getMock(); + $this->factory = $this->objectManager->getObject( + CreditmemoDocumentFactory::class, + [ + 'creditmemoFactory' => $this->creditmemoFactoryMock, + 'commentFactory' => $this->commentFactoryMock, + 'orderRepository' => $this->orderRepositoryMock + ] + ); + } + + private function commonFactoryFlow() + { + $this->creditmemoItemCreationMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn(7); + $this->creditmemoItemCreationMock->expects($this->once()) + ->method('getQty') + ->willReturn(3); + $this->commentCreationArgumentsMock->expects($this->once()) + ->method('getShippingAmount') + ->willReturn('20.00'); + $this->commentCreationMock->expects($this->once()) + ->method('getComment') + ->willReturn('text'); + $this->commentFactoryMock->expects($this->once()) + ->method('create') + ->with( + [ + 'data' => [ + 'comment' => 'text', + 'is_visible_on_frontend' => null + ] + ] + ) + ->willReturn($this->commentMock); + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(11); + $this->creditmemoMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('setParentId') + ->with(11) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setStoreId') + ->with(1) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setIsCustomerNotified') + ->with(true) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('setCreditmemo') + ->with($this->creditmemoMock) + ->willReturnSelf(); + } + + public function testCreateFromOrder() + { + $this->commonFactoryFlow(); + $this->creditmemoFactoryMock->expects($this->once()) + ->method('createByOrder') + ->with( + $this->orderMock, + [ + 'shipping_amount' => '20.00', + 'qtys' => [7 => 3], + 'adjustment_positive' => null, + 'adjustment_negative' => null + ] + ) + ->willReturn($this->creditmemoMock); + $this->factory->createFromOrder( + $this->orderMock, + [$this->creditmemoItemCreationMock], + $this->commentCreationMock, + true, + $this->commentCreationArgumentsMock + ); + } + + public function testCreateFromInvoice() + { + $this->commonFactoryFlow(); + $this->creditmemoFactoryMock->expects($this->once()) + ->method('createByInvoice') + ->with( + $this->invoiceMock, + [ + 'shipping_amount' => '20.00', + 'qtys' => [7 => 3], + 'adjustment_positive' => null, + 'adjustment_negative' => null + ] + ) + ->willReturn($this->creditmemoMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->invoiceMock->expects($this->once()) + ->method('setOrder') + ->with($this->orderMock) + ->willReturnSelf(); + $this->factory->createFromInvoice( + $this->invoiceMock, + [$this->creditmemoItemCreationMock], + $this->commentCreationMock, + true, + $this->commentCreationArgumentsMock + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php new file mode 100644 index 0000000000000..773f3b75c91f5 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Validation/CanRefundTest.php @@ -0,0 +1,131 @@ +invoiceMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Invoice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderPaymentRepositoryMock = $this->getMockBuilder( + \Magento\Sales\Api\OrderPaymentRepositoryInterface::class + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentMock = $this->getMockBuilder(\Magento\Payment\Model\InfoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->validator = new \Magento\Sales\Model\Order\Invoice\Validation\CanRefund( + $this->orderPaymentRepositoryMock, + $this->orderRepositoryMock + ); + } + + public function testValidateWrongInvoiceState() + { + $this->invoiceMock->expects($this->exactly(2)) + ->method('getState') + ->willReturnOnConsecutiveCalls( + \Magento\Sales\Model\Order\Invoice::STATE_OPEN, + \Magento\Sales\Model\Order\Invoice::STATE_CANCELED + ); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + } + + public function testValidateInvoiceSumWasRefunded() + { + $this->invoiceMock->expects($this->once()) + ->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Invoice::STATE_PAID); + $this->invoiceMock->expects($this->once()) + ->method('getBaseGrandTotal') + ->willReturn(1); + $this->invoiceMock->expects($this->once()) + ->method('getBaseTotalRefunded') + ->willReturn(1); + $this->assertEquals( + [__('We can\'t create creditmemo for the invoice.')], + $this->validator->validate($this->invoiceMock) + ); + } + + public function testValidate() + { + $this->invoiceMock->expects($this->once()) + ->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Invoice::STATE_PAID); + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($orderMock); + $orderMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + $methodInstanceMock = $this->getMockBuilder(\Magento\Payment\Model\MethodInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentMock->expects($this->once()) + ->method('getMethodInstance') + ->willReturn($methodInstanceMock); + $methodInstanceMock->expects($this->atLeastOnce()) + ->method('canRefund') + ->willReturn(true); + $this->invoiceMock->expects($this->once()) + ->method('getBaseGrandTotal') + ->willReturn(1); + $this->invoiceMock->expects($this->once()) + ->method('getBaseTotalRefunded') + ->willReturn(0); + $this->assertEquals( + [], + $this->validator->validate($this->invoiceMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php index 2da2f4bba3f1a..8d4daec7bf8d5 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php @@ -16,10 +16,20 @@ class PaymentAdapterTest extends \PHPUnit_Framework_TestCase private $subject; /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\Data\OrderInterface|\PHPUnit_Framework_MockObject_MockObject */ private $orderMock; + /** + * @var \Magento\Sales\Api\Data\CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoMock; + + /** + * @var \Magento\Sales\Model\Order\Creditmemo\RefundOperation|\PHPUnit_Framework_MockObject_MockObject + */ + private $refundOperationMock; + /** * @var \Magento\Sales\Api\Data\InvoiceInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -36,6 +46,14 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->refundOperationMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Creditmemo\RefundOperation::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -45,10 +63,24 @@ protected function setUp() ->getMock(); $this->subject = new \Magento\Sales\Model\Order\PaymentAdapter( + $this->refundOperationMock, $this->payOperationMock ); } + public function testRefund() + { + $isOnline = true; + $this->refundOperationMock->expects($this->once()) + ->method('execute') + ->with($this->creditmemoMock, $this->orderMock, $isOnline) + ->willReturn($this->orderMock); + $this->assertEquals( + $this->orderMock, + $this->subject->refund($this->creditmemoMock, $this->orderMock, $isOnline) + ); + } + public function testPay() { $isOnline = true; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php new file mode 100644 index 0000000000000..0b4246d469444 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanRefundTest.php @@ -0,0 +1,113 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStatus', 'getItems']) + ->getMockForAbstractClass(); + + $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->priceCurrencyMock->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + $this->model = new \Magento\Sales\Model\Order\Validation\CanRefund( + $this->priceCurrencyMock + ); + } + + /** + * @param string $state + * + * @dataProvider canCreditmemoWrongStateDataProvider + */ + public function testCanCreditmemoWrongState($state) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn($state); + $this->orderMock->expects($this->once()) + ->method('getStatus') + ->willReturn('status'); + $this->orderMock->expects($this->never()) + ->method('getTotalPaid') + ->willReturn(15); + $this->orderMock->expects($this->never()) + ->method('getTotalRefunded') + ->willReturn(14); + $this->assertEquals( + [__('A creditmemo can not be created when an order has a status of %1', 'status')], + $this->model->validate($this->orderMock) + ); + } + + /** + * Data provider for testCanCreditmemoWrongState + * @return array + */ + public function canCreditmemoWrongStateDataProvider() + { + return [ + [Order::STATE_PAYMENT_REVIEW], + [Order::STATE_HOLDED], + [Order::STATE_CANCELED], + [Order::STATE_CLOSED], + ]; + } + + public function testCanCreditmemoNoMoney() + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + $this->orderMock->expects($this->once()) + ->method('getTotalPaid') + ->willReturn(15); + $this->orderMock->expects($this->once()) + ->method('getTotalRefunded') + ->willReturn(15); + $this->assertEquals( + [ + __('The order does not allow a creditmemo to be created.') + ], + $this->model->validate($this->orderMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php new file mode 100644 index 0000000000000..9a0a9e4f8c688 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -0,0 +1,460 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->notifierMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCommentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->refundInvoice = new RefundInvoice( + $this->resourceConnectionMock, + $this->orderStateResolverMock, + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->orderValidatorMock, + $this->invoiceValidatorMock, + $this->creditmemoValidatorMock, + $this->creditmemoRepositoryMock, + $this->paymentAdapterMock, + $this->creditmemoDocumentFactoryMock, + $this->notifierMock, + $this->configMock, + $this->loggerMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willReturn($this->orderMock); + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, []) + ->willReturn(Order::STATE_CLOSED); + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_CLOSED) + ->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_CLOSED); + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_CLOSED) + ->willReturn('Closed'); + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Closed') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('setState') + ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) + ->willReturnSelf(); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->creditmemoMock) + ->willReturn($this->creditmemoMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + if ($notify) { + $this->notifierMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->creditmemoMock, $this->creditmemoCommentCreationMock); + } + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $invoiceId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + + $this->assertEquals( + $errorMessages, + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotRefundExceptionInterface + */ + public function testCouldNotCreditmemoException() + { + $invoiceId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->invoiceMock); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromInvoice') + ->with( + $this->invoiceMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock) + ->willReturn([]); + $e = new \Exception(); + + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willThrowException($e); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->adapterInterface->expects($this->once()) + ->method('rollBack'); + + $this->refundInvoice->execute( + $invoiceId, + $items, + false, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'TestWithNotifyTrue' => [1, [1 => 2], true, true], + 'TestWithNotifyFalse' => [1, [1 => 2], false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php new file mode 100644 index 0000000000000..a50dc31d80a84 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -0,0 +1,414 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->notifierMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCommentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->refundOrder = new RefundOrder( + $this->resourceConnectionMock, + $this->orderStateResolverMock, + $this->orderRepositoryMock, + $this->orderValidatorMock, + $this->creditmemoValidatorMock, + $this->creditmemoRepositoryMock, + $this->paymentAdapterMock, + $this->creditmemoDocumentFactoryMock, + $this->notifierMock, + $this->configMock, + $this->loggerMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testOrderCreditmemo($orderId, $items, $notify, $appendComment) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willReturn($this->orderMock); + + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, []) + ->willReturn(Order::STATE_CLOSED); + + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_CLOSED) + ->willReturnSelf(); + + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_CLOSED); + + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_CLOSED) + ->willReturn('Closed'); + + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Closed') + ->willReturnSelf(); + + $this->creditmemoMock->expects($this->once()) + ->method('setState') + ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) + ->willReturnSelf(); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->creditmemoMock) + ->willReturn($this->creditmemoMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + + if ($notify) { + $this->notifierMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->creditmemoMock, $this->creditmemoCommentCreationMock); + } + + $this->creditmemoMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $orderId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + + $this->assertEquals( + $errorMessages, + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotRefundExceptionInterface + */ + public function testCouldNotCreditmemoException() + { + $orderId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->creditmemoDocumentFactoryMock->expects($this->once()) + ->method('createFromOrder') + ->with( + $this->orderMock, + $items, + $this->creditmemoCommentCreationMock, + ($appendComment && $notify), + $this->creditmemoCreationArgumentsMock + )->willReturn($this->creditmemoMock); + + $this->creditmemoValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->creditmemoMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $e = new \Exception(); + + $this->paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($this->creditmemoMock, $this->orderMock) + ->willThrowException($e); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->adapterInterface->expects($this->once()) + ->method('rollBack'); + + $this->refundOrder->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'TestWithNotifyTrue' => [1, [1 => 2], true, true], + 'TestWithNotifyFalse' => [1, [1 => 2], false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php index a9fe8e4a1ed35..c0593fede2648 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php @@ -5,10 +5,11 @@ */ namespace Magento\Sales\Test\Unit\Model\Service; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Order; /** * Class CreditmemoServiceTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CreditmemoServiceTest extends \PHPUnit_Framework_TestCase { @@ -37,64 +38,74 @@ class CreditmemoServiceTest extends \PHPUnit_Framework_TestCase */ protected $creditmemoNotifierMock; + /** + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceCurrencyMock; + /** * @var \Magento\Sales\Model\Service\CreditmemoService */ protected $creditmemoService; + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + /** * SetUp */ protected function setUp() { - $objectManager = new ObjectManagerHelper($this); - $this->creditmemoRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Sales\Api\CreditmemoRepositoryInterface', + \Magento\Sales\Api\CreditmemoRepositoryInterface::class, ['get'], '', false ); $this->creditmemoCommentRepositoryMock = $this->getMockForAbstractClass( - 'Magento\Sales\Api\CreditmemoCommentRepositoryInterface', + \Magento\Sales\Api\CreditmemoCommentRepositoryInterface::class, [], '', false ); $this->searchCriteriaBuilderMock = $this->getMock( - 'Magento\Framework\Api\SearchCriteriaBuilder', + \Magento\Framework\Api\SearchCriteriaBuilder::class, ['create', 'addFilters'], [], '', false ); $this->filterBuilderMock = $this->getMock( - 'Magento\Framework\Api\FilterBuilder', + \Magento\Framework\Api\FilterBuilder::class, ['setField', 'setValue', 'setConditionType', 'create'], [], '', false ); $this->creditmemoNotifierMock = $this->getMock( - 'Magento\Sales\Model\Order\CreditmemoNotifier', + \Magento\Sales\Model\Order\CreditmemoNotifier::class, [], [], '', false ); - - $this->creditmemoService = $objectManager->getObject( - 'Magento\Sales\Model\Service\CreditmemoService', + $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->creditmemoService = $this->objectManagerHelper->getObject( + \Magento\Sales\Model\Service\CreditmemoService::class, [ 'creditmemoRepository' => $this->creditmemoRepositoryMock, 'creditmemoCommentRepository' => $this->creditmemoCommentRepositoryMock, 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'filterBuilder' => $this->filterBuilderMock, - 'creditmemoNotifier' => $this->creditmemoNotifierMock + 'creditmemoNotifier' => $this->creditmemoNotifierMock, + 'priceCurrency' => $this->priceCurrencyMock ] ); } - /** * Run test cancel method * @expectedExceptionMessage You can not cancel Credit Memo @@ -104,7 +115,6 @@ public function testCancel() { $this->assertTrue($this->creditmemoService->cancel(1)); } - /** * Run test getCommentsList method */ @@ -112,22 +122,20 @@ public function testGetCommentsList() { $id = 25; $returnValue = 'return-value'; - $filterMock = $this->getMock( - 'Magento\Framework\Api\Filter', + \Magento\Framework\Api\Filter::class, [], [], '', false ); $searchCriteriaMock = $this->getMock( - 'Magento\Framework\Api\SearchCriteria', + \Magento\Framework\Api\SearchCriteria::class, [], [], '', false ); - $this->filterBuilderMock->expects($this->once()) ->method('setField') ->with('parent_id') @@ -153,10 +161,8 @@ public function testGetCommentsList() ->method('getList') ->with($searchCriteriaMock) ->will($this->returnValue($returnValue)); - $this->assertEquals($returnValue, $this->creditmemoService->getCommentsList($id)); } - /** * Run test notify method */ @@ -164,14 +170,12 @@ public function testNotify() { $id = 123; $returnValue = 'return-value'; - $modelMock = $this->getMockForAbstractClass( - 'Magento\Sales\Model\AbstractModel', + \Magento\Sales\Model\AbstractModel::class, [], '', false ); - $this->creditmemoRepositoryMock->expects($this->once()) ->method('get') ->with($id) @@ -179,8 +183,128 @@ public function testNotify() $this->creditmemoNotifierMock->expects($this->once()) ->method('notify') ->with($modelMock) - ->will($this->returnValue($returnValue)); - + ->will($this->returnValue($returnValue)); $this->assertEquals($returnValue, $this->creditmemoService->notify($id)); } + public function testRefund() + { + $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder', 'getInvoice']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditMemoMock->expects($this->once())->method('getId')->willReturn(null); + $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); + $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); + $orderMock->expects($this->once())->method('getBaseTotalRefunded')->willReturn(0); + $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); + $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); + $this->priceCurrencyMock->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + // Set payment adapter dependency + $paymentAdapterMock = $this->getMockBuilder(\Magento\Sales\Model\Order\PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->setBackwardCompatibleProperty( + $this->creditmemoService, + 'paymentAdapter', + $paymentAdapterMock + ); + // Set resource dependency + $resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->setBackwardCompatibleProperty( + $this->creditmemoService, + 'resource', + $resourceMock + ); + // Set order repository dependency + $orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->setBackwardCompatibleProperty( + $this->creditmemoService, + 'orderRepository', + $orderRepositoryMock + ); + $adapterMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $resourceMock->expects($this->once())->method('getConnection')->with('sales')->willReturn($adapterMock); + $adapterMock->expects($this->once())->method('beginTransaction'); + $paymentAdapterMock->expects($this->once()) + ->method('refund') + ->with($creditMemoMock, $orderMock, false) + ->willReturn($orderMock); + $orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($orderMock); + $creditMemoMock->expects($this->once()) + ->method('getInvoice') + ->willReturn(null); + $adapterMock->expects($this->once())->method('commit'); + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save'); + $this->assertSame($creditMemoMock, $this->creditmemoService->refund($creditMemoMock, true)); + } + /** + * @expectedExceptionMessage The most money available to refund is 1. + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testRefundExpectsMoneyAvailableToReturn() + { + $baseGrandTotal = 10; + $baseTotalRefunded = 9; + $baseTotalPaid = 10; + $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder', 'formatBasePrice']) + ->getMockForAbstractClass(); + $creditMemoMock->expects($this->once())->method('getId')->willReturn(null); + $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); + $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); + $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn($baseGrandTotal); + $orderMock->expects($this->atLeastOnce())->method('getBaseTotalRefunded')->willReturn($baseTotalRefunded); + $this->priceCurrencyMock->expects($this->exactly(2))->method('round')->withConsecutive( + [$baseTotalRefunded + $baseGrandTotal], + [$baseTotalPaid] + )->willReturnOnConsecutiveCalls( + $baseTotalRefunded + $baseGrandTotal, + $baseTotalPaid + ); + $orderMock->expects($this->atLeastOnce())->method('getBaseTotalPaid')->willReturn($baseTotalPaid); + $baseAvailableRefund = $baseTotalPaid - $baseTotalRefunded; + $orderMock->expects($this->once())->method('formatBasePrice')->with( + $baseAvailableRefund + )->willReturn($baseAvailableRefund); + $this->creditmemoService->refund($creditMemoMock, true); + } + /** + * @expectedExceptionMessage We cannot register an existing credit memo. + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testRefundDoNotExpectsId() + { + $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $creditMemoMock->expects($this->once())->method('getId')->willReturn(444); + $this->creditmemoService->refund($creditMemoMock, true); + } + + /** + * Set mocked property + * + * @param object $object + * @param string $propertyName + * @param object $propertyValue + * @return void + */ + private function setBackwardCompatibleProperty($object, $propertyName, $propertyValue) + { + $reflection = new \ReflectionClass(get_class($object)); + $reflectionProperty = $reflection->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $propertyValue); + } } diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index f8a2e17d8a01b..8b13ab9e6d2a1 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -47,6 +47,8 @@ + + @@ -55,6 +57,7 @@ + @@ -99,6 +102,10 @@ + + + + @@ -439,7 +446,6 @@ Magento\Sales\Model\ResourceModel\Order\Creditmemo\Relation - Magento\Sales\Model\ResourceModel\Order\Creditmemo\Relation\Refund @@ -928,6 +934,13 @@ orderAddressMetadata + + + + Magento\Sales\Model\Order\Creditmemo\Sender\EmailSender + + + Magento\Sales\Model\Order\Payment\State\AuthorizeCommand diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index 5fa62ff7ebf26..dbbcc8170b04a 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -127,6 +127,12 @@ + + + + + + @@ -169,6 +175,12 @@ + + + + + + diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php new file mode 100644 index 0000000000000..2050e01cbfdd7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -0,0 +1,314 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->creditmemoRepository = $this->objectManager->get( + \Magento\Sales\Api\CreditmemoRepositoryInterface::class + ); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testShortRequest() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + ['orderId' => $existingOrder->getEntityId()] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + try { + $creditmemo = $this->creditmemoRepository->get($result); + + $expectedItems = $this->getOrderItems($existingOrder); + $actualCreditmemoItems = $this->getCreditmemoItems($creditmemo); + $actualRefundedOrderItems = $this->getRefundedOrderItems($updatedOrder); + + $this->assertEquals( + $expectedItems, + $actualCreditmemoItems, + 'Failed asserting that the Creditmemo contains all requested items' + ); + + $this->assertEquals( + $expectedItems, + $actualRefundedOrderItems, + 'Failed asserting that all requested order items were refunded' + ); + + $this->assertEquals( + $creditmemo->getShippingAmount(), + $existingOrder->getShippingAmount(), + 'Failed asserting that the Creditmemo contains correct shipping amount' + ); + + $this->assertEquals( + $creditmemo->getShippingAmount(), + $updatedOrder->getShippingRefunded(), + 'Failed asserting that proper shipping amount of the Order was refunded' + ); + + $this->assertNotEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that order status was changed' + ); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testFullRequest() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $expectedItems = $this->getOrderItems($existingOrder); + $expectedItems[0]['qty'] = $expectedItems[0]['qty'] - 1; + + $expectedComment = [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1 + ]; + + $expectedShippingAmount = 15; + $expectedAdjustmentPositive = 5.53; + $expectedAdjustmentNegative = 5.53; + + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $expectedItems, + 'comment' => $expectedComment, + 'arguments' => [ + 'shipping_amount' => $expectedShippingAmount, + 'adjustment_positive' => $expectedAdjustmentPositive, + 'adjustment_negative' => $expectedAdjustmentNegative + ] + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + try { + $creditmemo = $this->creditmemoRepository->get($result); + + $actualCreditmemoItems = $this->getCreditmemoItems($creditmemo); + $actualCreditmemoComment = $this->getRecentComment($creditmemo); + $actualRefundedOrderItems = $this->getRefundedOrderItems($updatedOrder); + + $this->assertEquals( + $expectedItems, + $actualCreditmemoItems, + 'Failed asserting that the Creditmemo contains all requested items' + ); + + $this->assertEquals( + $expectedItems, + $actualRefundedOrderItems, + 'Failed asserting that all requested order items were refunded' + ); + + $this->assertEquals( + $expectedComment, + $actualCreditmemoComment, + 'Failed asserting that the Creditmemo contains correct comment' + ); + + $this->assertEquals( + $expectedShippingAmount, + $creditmemo->getShippingAmount(), + 'Failed asserting that the Creditmemo contains correct shipping amount' + ); + + $this->assertEquals( + $expectedShippingAmount, + $updatedOrder->getShippingRefunded(), + 'Failed asserting that proper shipping amount of the Order was refunded' + ); + + $this->assertEquals( + $expectedAdjustmentPositive, + $creditmemo->getAdjustmentPositive(), + 'Failed asserting that the Creditmemo contains correct positive adjustment' + ); + + $this->assertEquals( + $expectedAdjustmentNegative, + $creditmemo->getAdjustmentNegative(), + 'Failed asserting that the Creditmemo contains correct negative adjustment' + ); + + $this->assertEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that order status was NOT changed' + ); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * Prepares and returns info for API service. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return array + */ + private function getServiceData(\Magento\Sales\Api\Data\OrderInterface $order) + { + return [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $order->getEntityId() . '/refund', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute', + ] + ]; + } + + /** + * Gets all items of given Order in proper format. + * + * @param \Magento\Sales\Model\Order $order + * + * @return array + */ + private function getOrderItems(\Magento\Sales\Model\Order $order) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $items[] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + } + + return $items; + } + + /** + * Gets refunded items of given Order in proper format. + * + * @param \Magento\Sales\Model\Order $order + * + * @return array + */ + private function getRefundedOrderItems(\Magento\Sales\Model\Order $order) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + if ($item->getQtyRefunded() > 0) { + $items[] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyRefunded(), + ]; + } + } + + return $items; + } + + /** + * Gets all items of given Creditmemo in proper format. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * + * @return array + */ + private function getCreditmemoItems(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo) + { + $items = []; + + /** @var \Magento\Sales\Api\Data\CreditmemoItemInterface $item */ + foreach ($creditmemo->getItems() as $item) { + $items[] = [ + 'order_item_id' => $item->getOrderItemId(), + 'qty' => $item->getQty(), + ]; + } + + return $items; + } + + /** + * Gets the most recent comment of given Creditmemo in proper format. + * + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * + * @return array|null + */ + private function getRecentComment(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo) + { + $comments = $creditmemo->getComments(); + + if ($comments) { + $comment = reset($comments); + + return [ + 'comment' => $comment->getComment(), + 'is_visible_on_front' => $comment->getIsVisibleOnFront(), + ]; + } + + return null; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php new file mode 100644 index 0000000000000..3dfa428c4aad4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php @@ -0,0 +1,39 @@ +create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + +/** @var \Magento\Sales\Model\Service\InvoiceService $invoiceService */ +$invoiceService = $objectManager->create(\Magento\Sales\Api\InvoiceManagementInterface::class); + +/** @var \Magento\Framework\DB\Transaction $transaction */ +$transaction = $objectManager->create(\Magento\Framework\DB\Transaction::class); + +$order->setData( + 'base_to_global_rate', + 1 +)->setData( + 'base_to_order_rate', + 1 +)->setData( + 'shipping_amount', + 20 +)->setData( + 'base_shipping_amount', + 20 +); + +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); + +$order->setIsInProcess(true); + +$transaction->addObject($invoice)->addObject($order)->save(); From edc2d9381418fe9915df75b40988e1fa70e7d765 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Wed, 7 Sep 2016 18:10:39 +0300 Subject: [PATCH 02/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Class does not exists in 2.0 --- app/code/Magento/Sales/Model/Validator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Sales/Model/Validator.php b/app/code/Magento/Sales/Model/Validator.php index b8d57ded29702..3bdac2bc845c2 100644 --- a/app/code/Magento/Sales/Model/Validator.php +++ b/app/code/Magento/Sales/Model/Validator.php @@ -5,7 +5,7 @@ */ namespace Magento\Sales\Model; -use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; /** @@ -34,7 +34,7 @@ public function __construct(ObjectManagerInterface $objectManager) * @param object $entity * @param ValidatorInterface[] $validators * @return string[] - * @throws ConfigurationMismatchException + * @throws LocalizedException */ public function validate($entity, array $validators) { @@ -42,7 +42,7 @@ public function validate($entity, array $validators) foreach ($validators as $validatorName) { $validator = $this->objectManager->get($validatorName); if (!$validator instanceof ValidatorInterface) { - throw new ConfigurationMismatchException( + throw new LocalizedException( __( sprintf('Validator %s is not instance of general validator interface', $validatorName) ) From 48cd28ed3e8bb2a5c794848d5cda349a44956c50 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Thu, 8 Sep 2016 18:06:04 +0300 Subject: [PATCH 03/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. ValidationFix --- .../Validation/CreationQuantityValidator.php | 81 +++++++++++++++++++ .../Creditmemo/ItemCreationValidator.php | 40 +++++++++ .../ItemCreationValidatorInterface.php | 23 ++++++ .../Validation/QuantityValidator.php | 2 +- .../Magento/Sales/Model/RefundInvoice.php | 36 +++++++-- app/code/Magento/Sales/Model/RefundOrder.php | 23 +++++- app/code/Magento/Sales/Model/Validator.php | 12 ++- app/code/Magento/Sales/etc/di.xml | 1 + 8 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php create mode 100644 app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php new file mode 100644 index 0000000000000..5a9704866953c --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php @@ -0,0 +1,81 @@ +orderItemRepository = $orderItemRepository; + $this->context = $context; + } + + /** + * @inheritdoc + */ + public function validate($entity) + { + try { + $orderItem = $this->orderItemRepository->get($entity->getOrderItemId()); + if (!$this->isItemPartOfContextOrder($orderItem)) { + return [__('The creditmemo contains product item that is not part of the original order.')]; + } + } catch (NoSuchEntityException $e) { + return [__('The creditmemo contains product item that is not part of the original order.')]; + } + + if (!$this->isQtyAvailable($orderItem, $entity->getQty())) { + return [__('The quantity to refund must not be greater than the unrefunded quantity')]; + } + + return []; + } + + /** + * @param Item $orderItem + * @param int $qty + * @return bool + */ + private function isQtyAvailable(Item $orderItem, $qty) + { + return $qty <= $orderItem->getQtyToRefund() || $orderItem->isDummy(); + } + + /** + * @param OrderItemInterface $orderItem + * @return bool + */ + private function isItemPartOfContextOrder(OrderItemInterface $orderItem) + { + return $this->context instanceof OrderInterface && $this->context->getEntityId() === $orderItem->getOrderId(); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php new file mode 100644 index 0000000000000..f1b8e6d9cab9b --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidator.php @@ -0,0 +1,40 @@ +validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate( + CreditmemoItemCreationInterface $entity, + array $validators, + OrderInterface $context = null + ) { + return $this->validator->validate($entity, $validators, $context); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php new file mode 100644 index 0000000000000..9f8bb84ccd16a --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php @@ -0,0 +1,23 @@ +getOrderId() === null) { - return [__('Order Id is required for shipment document')]; + return [__('Order Id is required for creditmemo document')]; } $messages = []; diff --git a/app/code/Magento/Sales/Model/RefundInvoice.php b/app/code/Magento/Sales/Model/RefundInvoice.php index 18837315ef83a..832ef2bd514d3 100644 --- a/app/code/Magento/Sales/Model/RefundInvoice.php +++ b/app/code/Magento/Sales/Model/RefundInvoice.php @@ -12,7 +12,9 @@ use Magento\Sales\Api\RefundInvoiceInterface; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; use Magento\Sales\Model\Order\Creditmemo\Validation\TotalsValidator; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; @@ -64,6 +66,11 @@ class RefundInvoice implements RefundInvoiceInterface */ private $creditmemoValidator; + /** + * @var ItemCreationValidatorInterface + */ + private $itemCreationValidator; + /** * @var CreditmemoRepositoryInterface */ @@ -104,12 +111,13 @@ class RefundInvoice implements RefundInvoiceInterface * @param OrderValidatorInterface $orderValidator * @param InvoiceValidatorInterface $invoiceValidator * @param CreditmemoValidatorInterface $creditmemoValidator - * @param CreditmemoRepositoryInterface $creditmemoRepository - * @param PaymentAdapterInterface $paymentAdapter - * @param CreditmemoDocumentFactory $creditmemoDocumentFactory - * @param NotifierInterface $notifier - * @param OrderConfig $config - * @param LoggerInterface $logger + * @param Order\Creditmemo\ItemCreationValidatorInterface $itemCreationValidator + * @internal param CreditmemoRepositoryInterface $creditmemoRepository + * @internal param PaymentAdapterInterface $paymentAdapter + * @internal param CreditmemoDocumentFactory $creditmemoDocumentFactory + * @internal param NotifierInterface $notifier + * @internal param OrderConfig $config + * @internal param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -120,6 +128,7 @@ public function __construct( OrderValidatorInterface $orderValidator, InvoiceValidatorInterface $invoiceValidator, CreditmemoValidatorInterface $creditmemoValidator, + ItemCreationValidatorInterface $itemCreationValidator, CreditmemoRepositoryInterface $creditmemoRepository, PaymentAdapterInterface $paymentAdapter, CreditmemoDocumentFactory $creditmemoDocumentFactory, @@ -133,6 +142,7 @@ public function __construct( $this->invoiceRepository = $invoiceRepository; $this->orderValidator = $orderValidator; $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; $this->creditmemoRepository = $creditmemoRepository; $this->paymentAdapter = $paymentAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; @@ -183,10 +193,22 @@ public function execute( TotalsValidator::class ] ); + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation = array_merge( + $itemsValidation, + $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + ) + ); + } $validationMessages = array_merge( $orderValidationResult, $invoiceValidationResult, - $creditmemoValidationResult + $creditmemoValidationResult, + $itemsValidation ); if (!empty($validationMessages )) { throw new \Magento\Sales\Exception\DocumentValidationException( diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php index 70fc7be4310e6..65d81df0d1de9 100644 --- a/app/code/Magento/Sales/Model/RefundOrder.php +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -11,7 +11,9 @@ use Magento\Sales\Api\RefundOrderInterface; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; use Magento\Sales\Model\Order\Creditmemo\Validation\TotalsValidator; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; @@ -52,6 +54,11 @@ class RefundOrder implements RefundOrderInterface */ private $creditmemoValidator; + /** + * @var Order\Creditmemo\ItemCreationValidatorInterface + */ + private $itemCreationValidator; + /** * @var CreditmemoRepositoryInterface */ @@ -89,6 +96,7 @@ class RefundOrder implements RefundOrderInterface * @param OrderRepositoryInterface $orderRepository * @param OrderValidatorInterface $orderValidator * @param CreditmemoValidatorInterface $creditmemoValidator + * @param ItemCreationValidatorInterface $itemCreationValidator * @param CreditmemoRepositoryInterface $creditmemoRepository * @param PaymentAdapterInterface $paymentAdapter * @param CreditmemoDocumentFactory $creditmemoDocumentFactory @@ -103,6 +111,7 @@ public function __construct( OrderRepositoryInterface $orderRepository, OrderValidatorInterface $orderValidator, CreditmemoValidatorInterface $creditmemoValidator, + ItemCreationValidatorInterface $itemCreationValidator, CreditmemoRepositoryInterface $creditmemoRepository, PaymentAdapterInterface $paymentAdapter, CreditmemoDocumentFactory $creditmemoDocumentFactory, @@ -115,6 +124,7 @@ public function __construct( $this->orderRepository = $orderRepository; $this->orderValidator = $orderValidator; $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; $this->creditmemoRepository = $creditmemoRepository; $this->paymentAdapter = $paymentAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; @@ -156,7 +166,18 @@ public function execute( TotalsValidator::class ] ); - $validationMessages = array_merge($orderValidationResult, $creditmemoValidationResult); + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation = array_merge( + $itemsValidation, + $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + ) + ); + } + $validationMessages = array_merge($orderValidationResult, $creditmemoValidationResult, $itemsValidation); if (!empty($validationMessages)) { throw new \Magento\Sales\Exception\DocumentValidationException( __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) diff --git a/app/code/Magento/Sales/Model/Validator.php b/app/code/Magento/Sales/Model/Validator.php index 3bdac2bc845c2..c0914faf2ceea 100644 --- a/app/code/Magento/Sales/Model/Validator.php +++ b/app/code/Magento/Sales/Model/Validator.php @@ -33,14 +33,20 @@ public function __construct(ObjectManagerInterface $objectManager) /** * @param object $entity * @param ValidatorInterface[] $validators - * @return string[] + * @param object|null $context + * @return \string[] * @throws LocalizedException */ - public function validate($entity, array $validators) + public function validate($entity, array $validators, $context = null) { $messages = []; + $validatorArguments = []; + if ($context !== null) { + $validatorArguments['context'] = $context; + } + foreach ($validators as $validatorName) { - $validator = $this->objectManager->get($validatorName); + $validator = $this->objectManager->create($validatorName, $validatorArguments); if (!$validator instanceof ValidatorInterface) { throw new LocalizedException( __( diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 8b13ab9e6d2a1..3cc9067a542aa 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -103,6 +103,7 @@ + From 9a2cb51f30b002e697ff78294ce8c04b28467315 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Thu, 8 Sep 2016 18:57:39 +0300 Subject: [PATCH 04/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Fix Doc Comment --- app/code/Magento/Sales/Model/RefundInvoice.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Sales/Model/RefundInvoice.php b/app/code/Magento/Sales/Model/RefundInvoice.php index 832ef2bd514d3..37bf915aa505c 100644 --- a/app/code/Magento/Sales/Model/RefundInvoice.php +++ b/app/code/Magento/Sales/Model/RefundInvoice.php @@ -112,12 +112,12 @@ class RefundInvoice implements RefundInvoiceInterface * @param InvoiceValidatorInterface $invoiceValidator * @param CreditmemoValidatorInterface $creditmemoValidator * @param Order\Creditmemo\ItemCreationValidatorInterface $itemCreationValidator - * @internal param CreditmemoRepositoryInterface $creditmemoRepository - * @internal param PaymentAdapterInterface $paymentAdapter - * @internal param CreditmemoDocumentFactory $creditmemoDocumentFactory - * @internal param NotifierInterface $notifier - * @internal param OrderConfig $config - * @internal param LoggerInterface $logger + * @param CreditmemoRepositoryInterface $creditmemoRepository + * @param PaymentAdapterInterface $paymentAdapter + * @param CreditmemoDocumentFactory $creditmemoDocumentFactory + * @param NotifierInterface $notifier + * @param OrderConfig $config + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( From afa03761483fbee30fa35065c9a18545ed4c7c89 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 12 Sep 2016 16:45:28 +0300 Subject: [PATCH 05/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Static fixes --- .../Magento/Sales/Model/Order/Creditmemo.php | 15 ++- .../Validation/CreationQuantityValidator.php | 2 +- .../CreateQuantityValidatorTest.php | 118 ++++++++++++++++++ .../Validation/QuantityValidatorTest.php | 2 +- .../Test/Unit/Model/RefundInvoiceTest.php | 8 +- .../Sales/Test/Unit/Model/RefundOrderTest.php | 71 ++++++----- 6 files changed, 175 insertions(+), 41 deletions(-) create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index b40b09a4883d4..9b93d6b50a185 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -532,11 +532,24 @@ public function setAdjustmentNegative($amount) */ public function isLast() { - foreach ($this->getAllItems() as $item) { + $items = $this->getAllItems(); + foreach ($items as $item) { if (!$item->isLast()) { return false; } } + + if (empty($items)) { + $order = $this->getOrder(); + if ($order) { + foreach ($order->getItems() as $orderItem) { + if ($orderItem->canRefund()) { + return false; + } + } + } + } + return true; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php index 5a9704866953c..edad1a2520632 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php @@ -54,7 +54,7 @@ public function validate($entity) } if (!$this->isQtyAvailable($orderItem, $entity->getQty())) { - return [__('The quantity to refund must not be greater than the unrefunded quantity')]; + return [__('The quantity to refund must not be greater than the unrefunded quantity.')]; } return []; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php new file mode 100644 index 0000000000000..1466a0f4fc9fe --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php @@ -0,0 +1,118 @@ +orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + + $this->orderItemMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->entity = $this->getMockBuilder(\stdClass::class) + ->disableOriginalConstructor() + ->setMethods(['getOrderItemId', 'getQty']) + ->getMock(); + } + + /** + * @dataProvider dataProvider + */ + public function testValidateCreditMemoProductItems($orderItemId, $expectedResult, $withContext = false) + { + if ($orderItemId) { + $this->entity->expects($this->once()) + ->method('getOrderItemId') + ->willReturn($orderItemId); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderItemId) + ->willReturn($this->orderItemMock); + } else { + $this->entity->expects($this->once()) + ->method('getOrderItemId') + ->willThrowException(new NoSuchEntityException()); + } + + $this->contexMock = null; + if ($withContext) { + $this->contexMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->entity->expects($this->once()) + ->method('getQty') + ->willReturn(11); + } + + $this->createQuantityValidator = new CreationQuantityValidator( + $this->orderItemRepositoryMock, + $this->contexMock + ); + + $this->assertEquals($expectedResult, $this->createQuantityValidator->validate($this->entity)); + } + + public function dataProvider() + { + return [ + 'testValidateCreditMemoProductItems' => [ + 1, + [__('The creditmemo contains product item that is not part of the original order.')], + ], + 'testValidateWithException' => [ + null, + [__('The creditmemo contains product item that is not part of the original order.')] + ], + 'testValidateWithContext' => [ + 1, + [__('The quantity to refund must not be greater than the unrefunded quantity.')], + true + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php index 5c3fa75c97af1..838c062956c24 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Validation/QuantityValidatorTest.php @@ -99,7 +99,7 @@ public function testValidateWithoutOrder() ->willReturn(null); $creditmemoMock->expects($this->never())->method('getItems'); $this->assertEquals( - [__('Order Id is required for shipment document')], + [__('Order Id is required for creditmemo document')], $this->validator->validate($creditmemoMock) ); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php index 9a0a9e4f8c688..31c8858a249a9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -139,31 +139,25 @@ protected function setUp() $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index a50dc31d80a84..7d684695664ba 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -16,6 +16,7 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; use Magento\Sales\Model\Order\OrderValidatorInterface; @@ -23,6 +24,7 @@ use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; use Magento\Sales\Model\RefundOrder; use Psr\Log\LoggerInterface; +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; /** * Class RefundOrderTest @@ -116,71 +118,72 @@ class RefundOrderTest extends \PHPUnit_Framework_TestCase */ private $loggerMock; + /** + * @var Order\Creditmemo\ItemCreationValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemCreationValidatorMock; + + /** + * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoItemCreationMock; + protected function setUp() { $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->configMock = $this->getMockBuilder(OrderConfig::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->notifierMock = $this->getMockBuilder(NotifierInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->creditmemoCommentCreationMock = $this->getMockBuilder(CreditmemoCommentCreationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->orderMock = $this->getMockBuilder(OrderInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->itemCreationValidatorMock = $this->getMockBuilder(Order\Creditmemo\ItemCreationValidatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->refundOrder = new RefundOrder( $this->resourceConnectionMock, @@ -188,6 +191,7 @@ protected function setUp() $this->orderRepositoryMock, $this->orderValidatorMock, $this->creditmemoValidatorMock, + $this->itemCreationValidatorMock, $this->creditmemoRepositoryMock, $this->paymentAdapterMock, $this->creditmemoDocumentFactoryMock, @@ -200,17 +204,16 @@ protected function setUp() /** * @dataProvider dataProvider */ - public function testOrderCreditmemo($orderId, $items, $notify, $appendComment) + public function testOrderCreditmemo($orderId, $notify, $appendComment) { + $items = [$this->creditmemoItemCreationMock]; $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with('sales') ->willReturn($this->adapterInterface); - $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); - $this->creditmemoDocumentFactoryMock->expects($this->once()) ->method('createFromOrder') ->with( @@ -220,7 +223,6 @@ public function testOrderCreditmemo($orderId, $items, $notify, $appendComment) ($appendComment && $notify), $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->creditmemoValidatorMock->expects($this->once()) ->method('validate') ->with($this->creditmemoMock) @@ -229,12 +231,17 @@ public function testOrderCreditmemo($orderId, $items, $notify, $appendComment) ->method('validate') ->with($this->orderMock) ->willReturn([]); - + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with( + reset($items), + [CreationQuantityValidator::class], + $this->orderMock + )->willReturn([]); $this->paymentAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willReturn($this->orderMock); - $this->orderStateResolverMock->expects($this->once()) ->method('getStateForOrder') ->with($this->orderMock, []) @@ -303,7 +310,7 @@ public function testOrderCreditmemo($orderId, $items, $notify, $appendComment) public function testDocumentValidationException() { $orderId = 1; - $items = [1 => 2]; + $items = [$this->creditmemoItemCreationMock]; $notify = true; $appendComment = true; $errorMessages = ['error1', 'error2']; @@ -330,6 +337,10 @@ public function testDocumentValidationException() ->method('validate') ->with($this->orderMock) ->willReturn([]); + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) + ->willReturn([]); $this->assertEquals( $errorMessages, @@ -350,18 +361,16 @@ public function testDocumentValidationException() public function testCouldNotCreditmemoException() { $orderId = 1; - $items = [1 => 2]; + $items = [$this->creditmemoItemCreationMock]; $notify = true; $appendComment = true; $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with('sales') ->willReturn($this->adapterInterface); - $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); - $this->creditmemoDocumentFactoryMock->expects($this->once()) ->method('createFromOrder') ->with( @@ -371,7 +380,10 @@ public function testCouldNotCreditmemoException() ($appendComment && $notify), $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - + $this->itemCreationValidatorMock->expects($this->once()) + ->method('validate') + ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) + ->willReturn([]); $this->creditmemoValidatorMock->expects($this->once()) ->method('validate') ->with($this->creditmemoMock) @@ -381,16 +393,13 @@ public function testCouldNotCreditmemoException() ->with($this->orderMock) ->willReturn([]); $e = new \Exception(); - $this->paymentAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willThrowException($e); - $this->loggerMock->expects($this->once()) ->method('critical') ->with($e); - $this->adapterInterface->expects($this->once()) ->method('rollBack'); @@ -407,8 +416,8 @@ public function testCouldNotCreditmemoException() public function dataProvider() { return [ - 'TestWithNotifyTrue' => [1, [1 => 2], true, true], - 'TestWithNotifyFalse' => [1, [1 => 2], false, true], + 'TestWithNotifyTrue' => [1, true, true], + 'TestWithNotifyFalse' => [1, false, true], ]; } } From 661012e66f210d9248664c15707b0b9ec3b870ba Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Tue, 13 Sep 2016 16:32:42 +0300 Subject: [PATCH 06/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. L3 fixes --- .../Validation/QuantityValidator.php | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php index 3a34b5351288b..82fd0479166e5 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Validation/QuantityValidator.php @@ -188,35 +188,14 @@ private function isQtyAvailable(Item $orderItem, $qty) * @param double $qty * @param array $invoiceQtysRefundLimits * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function canRefundItem(\Magento\Sales\Model\Order\Item $item, $qty, $invoiceQtysRefundLimits) + private function canRefundItem(\Magento\Sales\Model\Order\Item $item, $qty, array $invoiceQtysRefundLimits) { if ($item->isDummy()) { - if ($item->getHasChildren()) { - foreach ($item->getChildrenItems() as $child) { - if ($qty === null) { - if ($this->canRefundNoDummyItem($child, $invoiceQtysRefundLimits)) { - return true; - } - } else { - if ($qty > 0) { - return true; - } - } - } - return false; - } elseif ($item->getParentItem()) { - $parent = $item->getParentItem(); - if ($qty === null) { - return $this->canRefundNoDummyItem($parent, $invoiceQtysRefundLimits); - } else { - return $qty > 0; - } - } - } else { - return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); + return $this->canRefundDummyItem($item, $qty, $invoiceQtysRefundLimits); } + + return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); } /** @@ -226,7 +205,7 @@ private function canRefundItem(\Magento\Sales\Model\Order\Item $item, $qty, $inv * @param array $invoiceQtysRefundLimits * @return bool */ - private function canRefundNoDummyItem($item, $invoiceQtysRefundLimits = []) + private function canRefundNoDummyItem(\Magento\Sales\Model\Order\Item $item, array $invoiceQtysRefundLimits = []) { if ($item->getQtyToRefund() < 0) { return false; @@ -236,4 +215,39 @@ private function canRefundNoDummyItem($item, $invoiceQtysRefundLimits = []) } return true; } + + /** + * @param Item $item + * @param int $qty + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundDummyItem(\Magento\Sales\Model\Order\Item $item, $qty, array $invoiceQtysRefundLimits) + { + if ($item->getHasChildren()) { + foreach ($item->getChildrenItems() as $child) { + if ($this->canRefundRequestedQty($child, $qty, $invoiceQtysRefundLimits)) { + return true; + } + } + } elseif ($item->getParentItem()) { + return $this->canRefundRequestedQty($item->getParentItem(), $qty, $invoiceQtysRefundLimits); + } + + return false; + } + + /** + * @param Item $item + * @param int $qty + * @param array $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundRequestedQty( + \Magento\Sales\Model\Order\Item $item, + $qty, + array $invoiceQtysRefundLimits + ) { + return $qty === null ? $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits) : $qty > 0; + } } From 4f741ba1844eb39420e724f2053b608372316a0a Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk Date: Mon, 21 Mar 2016 12:41:57 +0200 Subject: [PATCH 07/48] MAGETWO-56001: Search box closes randomly on iOS devices - Backport for MAGETWO-50628 --- .../Search/view/frontend/web/form-mini.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/form-mini.js index 63a8b78603578..e81c735de6a7e 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/form-mini.js @@ -7,9 +7,10 @@ define([ 'jquery', 'underscore', 'mage/template', + "matchMedia", 'jquery/ui', 'mage/translate' -], function ($, _, mageTemplate) { +], function ($, _, mageTemplate, mediaCheck) { 'use strict'; /** @@ -38,7 +39,8 @@ define([ '' + '', submitBtn: 'button[type="submit"]', - searchLabel: '[data-role=minisearch-label]' + searchLabel: '[data-role=minisearch-label]', + isExpandable: null }, _create: function () { @@ -50,6 +52,7 @@ define([ this.searchForm = $(this.options.formSelector); this.submitBtn = this.searchForm.find(this.options.submitBtn)[0]; this.searchLabel = $(this.options.searchLabel); + this.isExpandable = this.options.isExpandable; _.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit'); @@ -57,11 +60,25 @@ define([ this.element.attr('autocomplete', this.options.autocomplete); + mediaCheck({ + media: '(max-width: 768px)', + entry: function () { + this.isExpandable = true; + }.bind(this), + exit: function () { + this.isExpandable = false; + this.element.removeAttr('aria-expanded'); + }.bind(this) + }); + this.element.on('blur', $.proxy(function () { setTimeout($.proxy(function () { if (this.autoComplete.is(':hidden')) { this.searchLabel.removeClass('active'); + if (this.isExpandable === true) { + this.element.attr('aria-expanded', 'false'); + } } this.autoComplete.hide(); this._updateAriaHasPopup(false); @@ -72,6 +89,9 @@ define([ this.element.on('focus', $.proxy(function () { this.searchLabel.addClass('active'); + if (this.isExpandable === true) { + this.element.attr('aria-expanded', 'true'); + } }, this)); this.element.on('keydown', this._onKeyDown); this.element.on('input propertychange', this._onPropertyChange); From 1054d6a8459ed8d0f79f5d7866675fb9a5b1ea8d Mon Sep 17 00:00:00 2001 From: Denys Rul Date: Wed, 25 May 2016 14:25:48 +0300 Subject: [PATCH 08/48] MAGETWO-56001: Search box closes randomly on iOS devices - Backport for MAGETWO-53263 --- .../Search/view/frontend/web/form-mini.js | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/form-mini.js index e81c735de6a7e..fd6adef4e4302 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/form-mini.js @@ -71,14 +71,18 @@ define([ }.bind(this) }); + this.searchLabel.on('click', function (e) { + // allow input to lose its' focus when clicking on label + if (this.isExpandable && this.isActive()) { + e.preventDefault(); + } + }.bind(this)); + this.element.on('blur', $.proxy(function () { setTimeout($.proxy(function () { if (this.autoComplete.is(':hidden')) { - this.searchLabel.removeClass('active'); - if (this.isExpandable === true) { - this.element.attr('aria-expanded', 'false'); - } + this.setActiveState(false); } this.autoComplete.hide(); this._updateAriaHasPopup(false); @@ -87,12 +91,7 @@ define([ this.element.trigger('blur'); - this.element.on('focus', $.proxy(function () { - this.searchLabel.addClass('active'); - if (this.isExpandable === true) { - this.element.attr('aria-expanded', 'true'); - } - }, this)); + this.element.on('focus', this.setActiveState.bind(this, true)); this.element.on('keydown', this._onKeyDown); this.element.on('input propertychange', this._onPropertyChange); @@ -101,6 +100,29 @@ define([ this._updateAriaHasPopup(false); }, this)); }, + + /** + * Checks if search field is active. + * + * @returns {Boolean} + */ + isActive: function () { + return this.searchLabel.hasClass('active'); + }, + + /** + * Sets state of the search field to provided value. + * + * @param {Boolean} isActive + */ + setActiveState: function (isActive) { + this.searchLabel.toggleClass('active', isActive); + + if (this.isExpandable) { + this.element.attr('aria-expanded', isActive); + } + }, + /** * @private * @return {Element} The first element in the suggestion list. From 3db4e5da2ca30c058b39211f4150453a7a3ad31e Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk Date: Mon, 14 Mar 2016 18:38:07 +0200 Subject: [PATCH 09/48] MAGETWO-56001: Search box closes randomly on iOS devices - Backport for "Bugfix: Unable to activate search form on phone" pull request --- app/code/Magento/Search/view/frontend/web/form-mini.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/form-mini.js index fd6adef4e4302..a5241a8a64cd6 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/form-mini.js @@ -79,6 +79,9 @@ define([ }.bind(this)); this.element.on('blur', $.proxy(function () { + if (!this.searchLabel.hasClass('active')) { + return; + } setTimeout($.proxy(function () { if (this.autoComplete.is(':hidden')) { From e4020e153a7906af90a4c21f41b37ba1858d73dd Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 17:17:49 +0300 Subject: [PATCH 10/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Controller/Account/EditPost.php | 82 ++- .../Customer/Controller/Address/FormPost.php | 29 +- .../Controller/Adminhtml/Index/Save.php | 131 +++-- .../Controller/Adminhtml/Index/Validate.php | 6 +- .../Controller/Adminhtml/Index/Viewfile.php | 7 +- .../Customer/Model/Customer/DataProvider.php | 106 +++- .../Customer/Model/CustomerExtractor.php | 21 +- .../Magento/Customer/Model/FileProcessor.php | 95 ++++ .../Customer/Model/Metadata/Form/File.php | 18 +- .../Customer/Model/Metadata/Form/Image.php | 2 +- .../Unit/Controller/Account/EditPostTest.php | 42 +- .../Unit/Controller/Address/FormPostTest.php | 31 +- .../Controller/Adminhtml/Index/SaveTest.php | 525 +++++++++++------- .../Adminhtml/Index/ValidateTest.php | 12 - .../Adminhtml/Index/ViewfileTest.php | 2 +- .../Unit/Model/Customer/DataProviderTest.php | 219 ++++++++ .../Test/Unit/Model/CustomerExtractorTest.php | 4 + .../Test/Unit/Model/FileProcessorTest.php | 135 +++++ .../Unit/Model/Metadata/Form/FileTest.php | 296 ++++++++-- .../Unit/Model/Metadata/Form/ImageTest.php | 264 ++++++++- .../Component/Form/Element/DataType/Media.php | 42 ++ .../Ui/view/base/web/js/form/element/media.js | 31 +- .../web/templates/form/element/media.html | 59 ++ 23 files changed, 1788 insertions(+), 371 deletions(-) create mode 100644 app/code/Magento/Customer/Model/FileProcessor.php create mode 100644 app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 4af9a62d54db3..a3891710ae5a7 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -6,6 +6,8 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Model\Customer\Mapper; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; @@ -20,6 +22,11 @@ */ class EditPost extends \Magento\Customer\Controller\AbstractAccount { + /** + * Form code for data extractor + */ + const FORM_DATA_EXTRACTOR_CODE = 'customer_account_edit'; + /** @var AccountManagementInterface */ protected $customerAccountManagement; @@ -37,6 +44,11 @@ class EditPost extends \Magento\Customer\Controller\AbstractAccount */ protected $session; + /** + * @var Mapper + */ + private $customerMapper; + /** * @param Context $context * @param Session $customerSession @@ -76,23 +88,19 @@ public function execute() } if ($this->getRequest()->isPost()) { - $customerId = $this->session->getCustomerId(); - $currentCustomer = $this->customerRepository->getById($customerId); - - // Prepare new customer data - $customer = $this->customerExtractor->extract('customer_account_edit', $this->_request); - $customer->setId($customerId); - if ($customer->getAddresses() == null) { - $customer->setAddresses($currentCustomer->getAddresses()); - } + $currentCustomerDataObject = $this->getCustomerDataObject($this->session->getCustomerId()); + $customerCandidateDataObject = $this->populateNewCustomerDataObject( + $this->_request, + $currentCustomerDataObject + ); // Change customer password if ($this->getRequest()->getParam('change_password')) { - $this->changeCustomerPassword($currentCustomer->getEmail()); + $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); } try { - $this->customerRepository->save($customer); + $this->customerRepository->save($customerCandidateDataObject); } catch (AuthenticationException $e) { $this->messageManager->addError($e->getMessage()); } catch (InputException $e) { @@ -116,6 +124,43 @@ public function execute() return $resultRedirect->setPath('*/*/edit'); } + /** + * Get customer data object + * + * @param int $customerId + * + * @return \Magento\Customer\Api\Data\CustomerInterface + */ + private function getCustomerDataObject($customerId) + { + return $this->customerRepository->getById($customerId); + } + + /** + * Create Data Transfer Object of customer candidate + * + * @param \Magento\Framework\App\RequestInterface $inputData + * @param \Magento\Customer\Api\Data\CustomerInterface $currentCustomerData + * @return \Magento\Customer\Api\Data\CustomerInterface + */ + private function populateNewCustomerDataObject( + \Magento\Framework\App\RequestInterface $inputData, + \Magento\Customer\Api\Data\CustomerInterface $currentCustomerData + ) { + $attributeValues = $this->getCustomerMapper()->toFlatArray($currentCustomerData); + $customerDto = $this->customerExtractor->extract( + self::FORM_DATA_EXTRACTOR_CODE, + $inputData, + $attributeValues + ); + $customerDto->setId($currentCustomerData->getId()); + if (!$customerDto->getAddresses()) { + $customerDto->setAddresses($currentCustomerData->getAddresses()); + } + + return $customerDto; + } + /** * Change customer password * @@ -148,4 +193,19 @@ protected function changeCustomerPassword($email) return $this; } + + /** + * Get Customer Mapper instance + * + * @return Mapper + * + * @deprecated + */ + private function getCustomerMapper() + { + if ($this->customerMapper === null) { + $this->customerMapper = ObjectManager::getInstance()->get('Magento\Customer\Model\Customer\Mapper'); + } + return $this->customerMapper; + } } diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index d581243cbba58..aaa94eec889bf 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -9,12 +9,14 @@ use Magento\Customer\Api\Data\AddressInterfaceFactory; use Magento\Customer\Api\Data\RegionInterface; use Magento\Customer\Api\Data\RegionInterfaceFactory; +use Magento\Customer\Model\Address\Mapper; use Magento\Customer\Model\Metadata\FormFactory; use Magento\Customer\Model\Session; use Magento\Directory\Helper\Data as HelperData; use Magento\Directory\Model\RegionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\ForwardFactory; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Exception\InputException; @@ -36,6 +38,11 @@ class FormPost extends \Magento\Customer\Controller\Address */ protected $helperData; + /** + * @var Mapper + */ + private $customerAddressMapper; + /** * @param Context $context * @param Session $customerSession @@ -127,12 +134,7 @@ protected function getExistingAddressData() if ($existingAddress->getCustomerId() !== $this->_getSession()->getCustomerId()) { throw new \Exception(); } - $existingAddressData = $this->_dataProcessor->buildOutputDataArray( - $existingAddress, - '\Magento\Customer\Api\Data\AddressInterface' - ); - $existingAddressData['region_code'] = $existingAddress->getRegion()->getRegionCode(); - $existingAddressData['region'] = $existingAddress->getRegion()->getRegion(); + $existingAddressData = $this->getCustomerAddressMapper()->toFlatArray($existingAddress); } return $existingAddressData; } @@ -214,4 +216,19 @@ public function execute() return $this->resultRedirectFactory->create()->setUrl($this->_redirect->error($url)); } + + /** + * Get Customer Address Mapper instance + * + * @return Mapper + * + * @deprecated + */ + private function getCustomerAddressMapper() + { + if ($this->customerAddressMapper === null) { + $this->customerAddressMapper = ObjectManager::getInstance()->get('Magento\Customer\Model\Address\Mapper'); + } + return $this->customerAddressMapper; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index bb5eb9eef9551..5f0f096a207d4 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -5,8 +5,11 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Controller\RegistryConstants; +use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Exception\LocalizedException; /** @@ -28,7 +31,7 @@ protected function _extractCustomerData() { $customerData = []; if ($this->getRequest()->getPost('customer')) { - $serviceAttributes = [ + $additionalAttributes = [ CustomerInterface::DEFAULT_BILLING, CustomerInterface::DEFAULT_SHIPPING, 'confirmation', @@ -36,10 +39,9 @@ protected function _extractCustomerData() ]; $customerData = $this->_extractData( - $this->getRequest(), 'adminhtml_customer', - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - $serviceAttributes, + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + $additionalAttributes, 'customer' ); } @@ -57,50 +59,47 @@ protected function _extractCustomerData() /** * Perform customer data filtration based on form code and form object * - * @param \Magento\Framework\App\RequestInterface $request * @param string $formCode The code of EAV form to take the list of attributes from * @param string $entityType entity type for the form * @param string[] $additionalAttributes The list of attribute codes to skip filtration for * @param string $scope scope of the request - * @param \Magento\Customer\Model\Metadata\Form $metadataForm to use for extraction - * @return array Filtered customer data + * @return array */ protected function _extractData( - \Magento\Framework\App\RequestInterface $request, $formCode, $entityType, $additionalAttributes = [], - $scope = null, - \Magento\Customer\Model\Metadata\Form $metadataForm = null + $scope = null ) { - if ($metadataForm === null) { - $metadataForm = $this->_formFactory->create( - $entityType, - $formCode, - [], - false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE - ); - } - $filteredData = $metadataForm->extractData($request, $scope); + $metadataForm = $this->getMetadataForm($entityType, $formCode, $scope); + $formData = $metadataForm->extractData($this->getRequest(), $scope); - $object = $this->_objectFactory->create(['data' => $request->getPostValue()]); + // Initialize additional attributes + /** @var \Magento\Framework\DataObject $object */ + $object = $this->_objectFactory->create(['data' => $this->getRequest()->getPostValue()]); $requestData = $object->getData($scope); foreach ($additionalAttributes as $attributeCode) { - $filteredData[$attributeCode] = isset($requestData[$attributeCode]) ? $requestData[$attributeCode] : false; + $formData[$attributeCode] = isset($requestData[$attributeCode]) ? $requestData[$attributeCode] : false; } + // Unset unused attributes $formAttributes = $metadataForm->getAttributes(); - /** @var \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute */ foreach ($formAttributes as $attribute) { + /** @var \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute */ $attributeCode = $attribute->getAttributeCode(); - $frontendInput = $attribute->getFrontendInput(); - if ($frontendInput != 'boolean' && $filteredData[$attributeCode] === false) { - unset($filteredData[$attributeCode]); + if ($attribute->getFrontendInput() != 'boolean' + && $formData[$attributeCode] === false + ) { + unset($formData[$attributeCode]); } } - return $filteredData; + $result = $metadataForm->compactData($formData); + + // Re-initialize additional attributes + $result = array_merge($result, array_diff_key($formData, $result)); + + return $result; } /** @@ -118,9 +117,8 @@ protected function saveDefaultFlags(array $addressIdList, array & $extractedCust foreach ($addressIdList as $addressId) { $scope = sprintf('address/%s', $addressId); $addressData = $this->_extractData( - $this->getRequest(), 'adminhtml_customer_address', - \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + AddressMetadataInterface::ENTITY_TYPE_ADDRESS, ['default_billing', 'default_shipping'], $scope ); @@ -180,18 +178,16 @@ public function execute() { $returnToEdit = false; $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = isset($originalRequestData['customer']['entity_id']) - ? $originalRequestData['customer']['entity_id'] - : null; + + $customerId = $this->getCurrentCustomerId(); + if ($originalRequestData) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); $addressesData = $this->_extractCustomerAddressData($customerData); - $request = $this->getRequest(); - $isExistingCustomer = (bool)$customerId; - $customer = $this->customerDataFactory->create(); - if ($isExistingCustomer) { + + if ($customerId) { $savedCustomerData = $this->_customerRepository->getById($customerId); $customerData = array_merge( $this->customerMapper->toFlatArray($savedCustomerData), @@ -200,6 +196,8 @@ public function execute() $customerData['id'] = $customerId; } + /** @var CustomerInterface $customer */ + $customer = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( $customer, $customerData, @@ -224,13 +222,13 @@ public function execute() $this->_eventManager->dispatch( 'adminhtml_customer_prepare_save', - ['customer' => $customer, 'request' => $request] + ['customer' => $customer, 'request' => $this->getRequest()] ); $customer->setAddresses($addresses); $customer->setStoreId($customerData['sendemail_store_id']); // Save customer - if ($isExistingCustomer) { + if ($customerId) { $this->_customerRepository->save($customer); } else { $customer = $this->customerAccountManagement->createAccount($customer); @@ -252,7 +250,7 @@ public function execute() // After save $this->_eventManager->dispatch( 'adminhtml_customer_save_after', - ['customer' => $customer, 'request' => $request] + ['customer' => $customer, 'request' => $this->getRequest()] ); $this->_getSession()->unsCustomerData(); // Done Saving customer, finish save action @@ -295,4 +293,59 @@ public function execute() } return $resultRedirect; } + + /** + * Get metadata form + * + * @param string $entityType + * @param string $formCode + * @param string $scope + * @return Form + */ + private function getMetadataForm($entityType, $formCode, $scope) + { + $attributeValues = []; + + if ($entityType == CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { + $customerId = $this->getCurrentCustomerId(); + if ($customerId) { + $customer = $this->_customerRepository->getById($customerId); + $attributeValues = $this->customerMapper->toFlatArray($customer); + } + } + + if ($entityType == AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { + $scopeData = explode('/', $scope); + if (isset($scopeData[1]) && is_numeric($scopeData[1])) { + $customerAddress = $this->addressRepository->getById($scopeData[1]); + $attributeValues = $this->addressMapper->toFlatArray($customerAddress); + } + } + + $metadataForm = $this->_formFactory->create( + $entityType, + $formCode, + $attributeValues, + false, + Form::DONT_IGNORE_INVISIBLE + ); + + return $metadataForm; + } + + /** + * Retrieve current customer ID + * + * @return int + */ + private function getCurrentCustomerId() + { + $originalRequestData = $this->getRequest()->getPostValue(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $customerId = isset($originalRequestData['entity_id']) + ? $originalRequestData['entity_id'] + : null; + + return $customerId; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php index 7c1f23881ec3d..2b3cbc2d2b6a0 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php @@ -28,11 +28,7 @@ protected function _validateCustomer($response) $customerForm = $this->_formFactory->create( 'customer', 'adminhtml_customer', - $this->_extensibleDataObjectConverter->toFlatArray( - $customer, - [], - '\Magento\Customer\Api\Data\CustomerInterface' - ), + [], true ); $customerForm->setInvisibleIgnored(true); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php index af512f7fa9f91..57c008445daf4 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php @@ -7,6 +7,7 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterfaceFactory; use Magento\Customer\Api\Data\CustomerInterfaceFactory; @@ -124,7 +125,7 @@ public function __construct( /** * Customer view file action * - * @return void + * @return \Magento\Framework\Controller\ResultInterface|void * @throws NotFoundException * * @SuppressWarnings(PHPMD.ExitExpression) @@ -151,7 +152,7 @@ public function execute() /** @var \Magento\Framework\Filesystem $filesystem */ $filesystem = $this->_objectManager->get('Magento\Framework\Filesystem'); $directory = $filesystem->getDirectoryRead(DirectoryList::MEDIA); - $fileName = 'customer' . '/' . ltrim($file, '/'); + $fileName = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . ltrim($file, '/'); $path = $directory->getAbsolutePath($fileName); if (!$directory->isFile($fileName) && !$this->_objectManager->get('Magento\MediaStorage\Helper\File\Storage')->processStorageFile($path) @@ -175,7 +176,7 @@ public function execute() $contentType = 'application/octet-stream'; break; } - $stat = $directory->stat($path); + $stat = $directory->stat($fileName); $contentLength = $stat['size']; $contentModify = $stat['mtime']; diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 45b34f3b38f09..1e7d359400baf 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -5,10 +5,14 @@ */ namespace Magento\Customer\Model\Customer; +use Magento\Customer\Model\Attribute; +use Magento\Customer\Model\FileProcessor; +use Magento\Customer\Model\FileProcessorFactory; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; use Magento\Customer\Model\Address; use Magento\Customer\Model\Customer; +use Magento\Framework\App\ObjectManager; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; @@ -71,8 +75,21 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider protected $eavValidationRules; /** - * Constructor + * @var FileProcessorFactory + */ + private $fileProcessorFactory; + + /** + * File types allowed for file uploader UI component * + * @var array + */ + private $fileUploaderTypes = [ + 'image', + 'file', + ]; + + /** * @param string $name * @param string $primaryFieldName * @param string $requestFieldName @@ -80,6 +97,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param CustomerCollectionFactory $customerCollectionFactory * @param Config $eavConfig * @param FilterPool $filterPool + * @param FileProcessorFactory $fileProcessorFactory * @param array $meta * @param array $data */ @@ -91,6 +109,7 @@ public function __construct( CustomerCollectionFactory $customerCollectionFactory, Config $eavConfig, FilterPool $filterPool, + FileProcessorFactory $fileProcessorFactory = null, array $meta = [], array $data = [] ) { @@ -100,6 +119,7 @@ public function __construct( $this->collection->addAttributeToSelect('*'); $this->eavConfig = $eavConfig; $this->filterPool = $filterPool; + $this->fileProcessorFactory = $fileProcessorFactory ?: $this->getFileProcessorFactory(); $this->meta['customer']['fields'] = $this->getAttributesMeta( $this->eavConfig->getEntityType('customer') ); @@ -122,6 +142,9 @@ public function getData() /** @var Customer $customer */ foreach ($items as $customer) { $result['customer'] = $customer->getData(); + + $this->overrideFileUploaderData($customer, $result['customer']); + unset($result['address']); /** @var Address $address */ @@ -130,6 +153,8 @@ public function getData() $address->load($addressId); $result['address'][$addressId] = $address->getData(); $this->prepareAddressData($addressId, $result['address'], $result['customer']); + + $this->overrideFileUploaderData($address, $result['address'][$addressId]); } $this->loadedData[$customer->getId()] = $result; } @@ -137,6 +162,69 @@ public function getData() return $this->loadedData; } + /** + * Override file uploader UI component data + * + * Overrides data for attributes with frontend_input equal to 'image' or 'file'. + * + * @param Customer|Address $entity + * @param array $entityData + */ + private function overrideFileUploaderData($entity, array &$entityData) + { + $attributes = $entity->getAttributes(); + foreach ($attributes as $attribute) { + /** @var Attribute $attribute */ + if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { + $entityData[$attribute->getAttributeCode()] = $this->getFileUploaderData( + $entity->getEntityType(), + $attribute, + $entityData + ); + } + } + } + + /** + * Retrieve array of values required by file uploader UI component + * + * @param Type $entityType + * @param Attribute $attribute + * @param array $customerData + * @return array + */ + private function getFileUploaderData( + Type $entityType, + Attribute $attribute, + array $customerData + ) { + $attributeCode = $attribute->getAttributeCode(); + + $file = isset($customerData[$attributeCode]) + ? $customerData[$attributeCode] + : ''; + + /** @var FileProcessor $fileProcessor */ + $fileProcessor = $this->getFileProcessorFactory()->create([ + 'entityTypeCode' => $entityType->getEntityTypeCode(), + ]); + + if (!empty($file) + && $fileProcessor->isExist($file) + ) { + $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); + } + + if (!empty($file)) { + return [ + 'file' => $file, + 'type' => $attribute->getFrontendInput(), + 'url' => isset($viewUrl) ? $viewUrl : '', + ]; + } + return []; + } + /** * Get attributes meta * @@ -197,4 +285,20 @@ protected function prepareAddressData($addressId, array &$addresses, array $cust $addresses[$addressId]['street'] = explode("\n", $addresses[$addressId]['street']); } } + + /** + * Get FileProcessorFactory instance + * + * @return FileProcessorFactory + * + * @deprecated + */ + private function getFileProcessorFactory() + { + if ($this->fileProcessorFactory === null) { + $this->fileProcessorFactory = ObjectManager::getInstance() + ->get('Magento\Customer\Model\FileProcessorFactory'); + } + return $this->fileProcessorFactory; + } } diff --git a/app/code/Magento/Customer/Model/CustomerExtractor.php b/app/code/Magento/Customer/Model/CustomerExtractor.php index 56e127def199e..197d75943c8dd 100644 --- a/app/code/Magento/Customer/Model/CustomerExtractor.php +++ b/app/code/Magento/Customer/Model/CustomerExtractor.php @@ -6,6 +6,8 @@ */ namespace Magento\Customer\Model; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\App\RequestInterface; @@ -60,12 +62,23 @@ public function __construct( /** * @param string $formCode * @param RequestInterface $request - * @return \Magento\Customer\Api\Data\CustomerInterface + * @param array $attributeValues + * @return CustomerInterface */ - public function extract($formCode, RequestInterface $request) - { - $customerForm = $this->formFactory->create('customer', $formCode); + public function extract( + $formCode, + RequestInterface $request, + array $attributeValues = [] + ) { + $customerForm = $this->formFactory->create( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + $formCode, + $attributeValues + ); + $customerData = $customerForm->extractData($request); + $customerData = $customerForm->compactData($customerData); + $allowedAttributes = $customerForm->getAllowedAttributes(); $isGroupIdEmpty = isset($allowedAttributes['group_id']); diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php new file mode 100644 index 0000000000000..f2aac7a295290 --- /dev/null +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -0,0 +1,95 @@ +mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->urlBuilder = $urlBuilder; + $this->urlEncoder = $urlEncoder; + $this->entityTypeCode = $entityTypeCode; + } + + /** + * Check if the file exists + * + * @param string $fileName + * @return bool + */ + public function isExist($fileName) + { + $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + + $result = $this->mediaDirectory->isExist($filePath); + return $result; + } + + /** + * Retrieve customer/index/viewfile action URL + * + * @param string $filePath + * @param string $type + * @return string + */ + public function getViewUrl($filePath, $type) + { + $viewUrl = ''; + + if ($this->entityTypeCode == AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { + $filePath = $this->entityTypeCode . '/' . ltrim($filePath, '/'); + $viewUrl = $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]) + . $this->mediaDirectory->getRelativePath($filePath); + } + + if ($this->entityTypeCode == CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { + $viewUrl = $this->urlBuilder->getUrl( + 'customer/index/viewfile', + [$type => $this->urlEncoder->encode(ltrim($filePath, '/'))] + ); + } + + return $viewUrl; + } +} diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 09a369bd50bf8..b94228cfa67b5 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -128,7 +128,9 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) } } - if (!empty($extend['delete'])) { + if (!empty($extend['delete']) + && filter_var($extend['delete'], FILTER_VALIDATE_BOOLEAN) + ) { $value['delete'] = true; } @@ -249,11 +251,11 @@ public function compactValue($value) return $this; } - $attribute = $this->getAttribute(); - $original = $this->_value; $toDelete = false; - if ($original) { - if (!$attribute->isRequired() && !empty($value['delete'])) { + if ($this->_value) { + if (!$this->getAttribute()->isRequired() + && !empty($value['delete']) + ) { $toDelete = true; } if (!empty($value['tmp_name'])) { @@ -262,11 +264,11 @@ public function compactValue($value) } $mediaDir = $this->_fileSystem->getDirectoryWrite(DirectoryList::MEDIA); - $result = $original; - // unlink entity file + $result = $this->_value; + if ($toDelete) { $result = ''; - $mediaDir->delete($this->_entityTypeCode . $original); + $mediaDir->delete($this->_entityTypeCode . '/' . ltrim($this->_value, '/')); } if (!empty($value['tmp_name'])) { diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index b50a2d519f4e0..003077d6c4c5f 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -68,7 +68,7 @@ protected function _validateByRules($value) $maxImageHeight = ArrayObjectSearch::getArrayElementByName( $rules, - 'max_image_height' + 'max_image_heght' ); if ($maxImageHeight !== null) { if ($maxImageHeight < $imageProp[1]) { diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php index b1af0f6d71d42..06a71ad5df02e 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php @@ -83,6 +83,11 @@ class EditPostTest extends \PHPUnit_Framework_TestCase */ protected $messageCollection; + /** + * @var \Magento\Customer\Model\Customer\Mapper|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerMapperMock; + protected function setUp() { $this->prepareContext(); @@ -109,6 +114,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->customerMapperMock = $this->getMockBuilder('Magento\Customer\Model\Customer\Mapper') + ->disableOriginalConstructor() + ->getMock(); + $this->model = new EditPost( $this->context, $this->session, @@ -117,6 +126,11 @@ protected function setUp() $this->validator, $this->customerExtractor ); + + $reflection = new \ReflectionClass(get_class($this->model)); + $reflectionProperty = $reflection->getProperty('customerMapper'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->model, $this->customerMapperMock); } public function testInvalidFormKey() @@ -160,7 +174,7 @@ public function testGeneralSave() $address = $this->getMockBuilder('Magento\Customer\Api\Data\AddressInterface') ->getMockForAbstractClass(); - $currentCustomerMock = $this->getCurrentCustomerMock($address); + $currentCustomerMock = $this->getCurrentCustomerMock($customerId, $address); $newCustomerMock = $this->getNewCustomerMock($customerId, $address); $this->validator->expects($this->once()) @@ -176,6 +190,11 @@ public function testGeneralSave() ->with('change_password') ->willReturn(false); + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($currentCustomerMock) + ->willReturn([]); + $this->session->expects($this->once()) ->method('getCustomerId') ->willReturn($customerId); @@ -234,13 +253,18 @@ public function testChangePassword( $address = $this->getMockBuilder('Magento\Customer\Api\Data\AddressInterface') ->getMockForAbstractClass(); - $currentCustomerMock = $this->getCurrentCustomerMock($address); + $currentCustomerMock = $this->getCurrentCustomerMock($customerId, $address); $currentCustomerMock->expects($this->once()) ->method('getEmail') ->willReturn($customerEmail); $newCustomerMock = $this->getNewCustomerMock($customerId, $address); + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($currentCustomerMock) + ->willReturn([]); + $this->validator->expects($this->once()) ->method('validate') ->with($this->request) @@ -387,9 +411,14 @@ public function testGeneralException( $address = $this->getMockBuilder('Magento\Customer\Api\Data\AddressInterface') ->getMockForAbstractClass(); - $currentCustomerMock = $this->getCurrentCustomerMock($address); + $currentCustomerMock = $this->getCurrentCustomerMock($customerId, $address); $newCustomerMock = $this->getNewCustomerMock($customerId, $address); + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($currentCustomerMock) + ->willReturn([]); + $exception = new $exception(__($message)); $this->validator->expects($this->once()) @@ -539,10 +568,11 @@ protected function getNewCustomerMock($customerId, $address) } /** + * @param int $customerId * @param \PHPUnit_Framework_MockObject_MockObject $address * @return \PHPUnit_Framework_MockObject_MockObject */ - protected function getCurrentCustomerMock($address) + protected function getCurrentCustomerMock($customerId, $address) { $currentCustomerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->getMockForAbstractClass(); @@ -551,6 +581,10 @@ protected function getCurrentCustomerMock($address) ->method('getAddresses') ->willReturn([$address]); + $currentCustomerMock->expects($this->any()) + ->method('getId') + ->willReturn($customerId); + return $currentCustomerMock; } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php index 183ff30b3cdfd..7775fa4ba4838 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php @@ -157,6 +157,11 @@ class FormPostTest extends \PHPUnit_Framework_TestCase */ protected $messageManager; + /** + * @var \Magento\Customer\Model\Address\Mapper|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerAddressMapper; + protected function setUp() { $this->prepareContext(); @@ -197,6 +202,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->customerAddressMapper = $this->getMockBuilder('Magento\Customer\Model\Address\Mapper') + ->disableOriginalConstructor() + ->getMock(); + $this->model = new FormPost( $this->context, $this->session, @@ -212,6 +221,11 @@ protected function setUp() $this->regionFactory, $this->helperData ); + + $reflection = new \ReflectionClass(get_class($this->model)); + $reflectionProperty = $reflection->getProperty('customerAddressMapper'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->model, $this->customerAddressMapper); } protected function prepareContext() @@ -459,22 +473,11 @@ public function testExecute( ->with($this->addressData) ->willReturnSelf(); - $this->dataProcessor->expects($this->once()) - ->method('buildOutputDataArray') - ->with($this->addressData, '\Magento\Customer\Api\Data\AddressInterface') + $this->customerAddressMapper->expects($this->once()) + ->method('toFlatArray') + ->with($this->addressData) ->willReturn($existingAddressData); - $this->addressData->expects($this->any()) - ->method('getRegion') - ->willReturn($this->regionData); - - $this->regionData->expects($this->once()) - ->method('getRegionCode') - ->willReturn($regionCode); - $this->regionData->expects($this->once()) - ->method('getRegion') - ->willReturn($region); - $this->formFactory->expects($this->once()) ->method('create') ->with('customer_address', 'customer_address_edit', $existingAddressData) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 89ad84f232860..7920c88b868e1 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -5,7 +5,12 @@ */ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Controller\Result\Redirect; /** @@ -130,6 +135,16 @@ class SaveTest extends \PHPUnit_Framework_TestCase */ protected $addressDataFactoryMock; + /** + * @var \Magento\Customer\Model\Address\Mapper|\PHPUnit_Framework_MockObject_MockObject + */ + protected $customerAddressMapperMock; + + /** + * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $customerAddressRepositoryMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -176,9 +191,15 @@ protected function setUp() $this->customerRepositoryMock = $this->getMockBuilder('Magento\Customer\Api\CustomerRepositoryInterface') ->disableOriginalConstructor() ->getMock(); + $this->customerAddressRepositoryMock = $this->getMockBuilder('Magento\Customer\Api\AddressRepositoryInterface') + ->disableOriginalConstructor() + ->getMock(); $this->customerMapperMock = $this->getMockBuilder('Magento\Customer\Model\Customer\Mapper') ->disableOriginalConstructor() ->getMock(); + $this->customerAddressMapperMock = $this->getMockBuilder('Magento\Customer\Model\Address\Mapper') + ->disableOriginalConstructor() + ->getMock(); $this->dataHelperMock = $this->getMockBuilder('Magento\Framework\Api\DataObjectHelper') ->disableOriginalConstructor() ->getMock(); @@ -234,6 +255,8 @@ protected function setUp() 'coreRegistry' => $this->registryMock, 'customerAccountManagement' => $this->managementMock, 'addressDataFactory' => $this->addressDataFactoryMock, + 'addressRepository' => $this->customerAddressRepositoryMock, + 'addressMapper' => $this->customerAddressMapperMock, ] ); } @@ -274,6 +297,15 @@ public function testExecuteWithExistentCustomer() 'coolness' => false, 'disable_auto_group_change' => 'false', ]; + $dataToCompact = [ + 'entity_id' => $customerId, + 'code' => 'value', + 'disable_auto_group_change' => 'false', + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + 'confirmation' => false, + 'sendemail_store_id' => false, + ]; $addressFilteredData = [ 'entity_id' => $addressId, 'default_billing' => 'true', @@ -283,12 +315,25 @@ public function testExecuteWithExistentCustomer() 'region' => 'region', 'region_id' => 'region_id', ]; + $addressDataToCompact = [ + 'entity_id' => $addressId, + 'default_billing' => 'true', + 'default_shipping' => 'true', + 'code' => 'value', + 'region' => 'region', + 'region_id' => 'region_id', + ]; $savedData = [ 'entity_id' => $customerId, 'darkness' => true, 'name' => 'Name', - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_BILLING => false, - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_SHIPPING => false, + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + ]; + $savedAddressData = [ + 'entity_id' => $addressId, + 'default_billing' => true, + 'default_shipping' => true, ]; $mergedData = [ 'entity_id' => $customerId, @@ -296,8 +341,8 @@ public function testExecuteWithExistentCustomer() 'name' => 'Name', 'code' => 'value', 'disable_auto_group_change' => 0, - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_BILLING => $addressId, - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_SHIPPING => $addressId, + CustomerInterface::DEFAULT_BILLING => $addressId, + CustomerInterface::DEFAULT_SHIPPING => $addressId, 'confirmation' => false, 'sendemail_store_id' => '1', 'id' => $customerId, @@ -316,7 +361,7 @@ public function testExecuteWithExistentCustomer() 'id' => $addressId, ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $formMock */ + /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $attributeMock */ $attributeMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') ->disableOriginalConstructor() ->getMock(); @@ -328,9 +373,13 @@ public function testExecuteWithExistentCustomer() ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->exactly(3)) + $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturn($postValue); + ->willReturnMap([ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ['address/' . $addressId, null, $postValue['address'][$addressId]], + ]); $this->requestMock->expects($this->exactly(3)) ->method('getPost') ->willReturnMap( @@ -341,69 +390,81 @@ public function testExecuteWithExistentCustomer() ] ); - /** @var \Magento\Customer\Model\Metadata\Form|\PHPUnit_Framework_MockObject_MockObject $formMock */ - $formMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ + $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') + ->disableOriginalConstructor() + ->getMock(); + $objectMock->expects($this->exactly(2)) + ->method('getData') + ->willReturnMap( + [ + ['customer', null, $postValue['customer']], + ['address/' . $addressId, null, $postValue['address'][$addressId]], + ] + ); + + $this->objectFactoryMock->expects($this->exactly(2)) + ->method('create') + ->with(['data' => $postValue]) + ->willReturn($objectMock); + + /** @var Form|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ + $customerFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + ->disableOriginalConstructor() + ->getMock(); + $customerFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'customer') + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('compactData') + ->with($dataToCompact) + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('getAttributes') + ->willReturn($attributes); + + $customerAddressFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') ->disableOriginalConstructor() ->getMock(); + $customerAddressFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'address/' . $addressId) + ->willReturn($addressFilteredData); + $customerAddressFormMock->expects($this->once()) + ->method('compactData') + ->with($addressDataToCompact) + ->willReturn($addressFilteredData); + $customerAddressFormMock->expects($this->once()) + ->method('getAttributes') + ->willReturn($attributes); $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturnMap( [ [ - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'adminhtml_customer', - [], + $savedData, false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE, + Form::DONT_IGNORE_INVISIBLE, [], - $formMock + $customerFormMock ], [ - \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + AddressMetadataInterface::ENTITY_TYPE_ADDRESS, 'adminhtml_customer_address', - [], + $savedAddressData, false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE, + Form::DONT_IGNORE_INVISIBLE, [], - $formMock + $customerAddressFormMock ], ] ); - $formMock->expects($this->exactly(2)) - ->method('extractData') - ->willReturnMap( - [ - [$this->requestMock, 'customer', true, $filteredData], - [$this->requestMock, 'address/' . $addressId, true, $addressFilteredData], - ] - ); - - /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ - $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') - ->disableOriginalConstructor() - ->getMock(); - - $this->objectFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); - - $objectMock->expects($this->exactly(2)) - ->method('getData') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], - ] - ); - - $formMock->expects($this->exactly(2)) - ->method('getAttributes') - ->willReturn($attributes); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ + /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ $customerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->disableOriginalConstructor() ->getMock(); @@ -412,12 +473,12 @@ public function testExecuteWithExistentCustomer() ->method('create') ->willReturn($customerMock); - $this->customerRepositoryMock->expects($this->once()) + $this->customerRepositoryMock->expects($this->exactly(2)) ->method('getById') ->with($customerId) ->willReturn($customerMock); - $this->customerMapperMock->expects($this->once()) + $this->customerMapperMock->expects($this->exactly(2)) ->method('toFlatArray') ->with($customerMock) ->willReturn($savedData); @@ -426,6 +487,16 @@ public function testExecuteWithExistentCustomer() ->disableOriginalConstructor() ->getMock(); + $this->customerAddressRepositoryMock->expects($this->once()) + ->method('getById') + ->with($addressId) + ->willReturn($addressMock); + + $this->customerAddressMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($addressMock) + ->willReturn($savedAddressData); + $this->addressDataFactoryMock->expects($this->once()) ->method('create') ->willReturn($addressMock); @@ -485,7 +556,7 @@ public function testExecuteWithExistentCustomer() $this->registryMock->expects($this->once()) ->method('register') - ->with(\Magento\Customer\Controller\RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + ->with(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); $this->messageManagerMock->expects($this->once()) ->method('addSuccess') @@ -547,6 +618,13 @@ public function testExecuteWithNewCustomer() 'coolness' => false, 'disable_auto_group_change' => 'false', ]; + $dataToCompact = [ + 'disable_auto_group_change' => 'false', + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + 'confirmation' => false, + 'sendemail_store_id' => false, + ]; $addressFilteredData = [ 'entity_id' => $addressId, 'default_billing' => 'false', @@ -556,10 +634,18 @@ public function testExecuteWithNewCustomer() 'region' => 'region', 'region_id' => 'region_id', ]; + $addressDataToCompact = [ + 'entity_id' => $addressId, + 'default_billing' => 'false', + 'default_shipping' => 'false', + 'code' => 'value', + 'region' => 'region', + 'region_id' => 'region_id', + ]; $mergedData = [ 'disable_auto_group_change' => 0, - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_BILLING => null, - \Magento\Customer\Api\Data\CustomerInterface::DEFAULT_SHIPPING => null, + CustomerInterface::DEFAULT_BILLING => null, + CustomerInterface::DEFAULT_SHIPPING => null, 'confirmation' => false, ]; $mergedAddressData = [ @@ -576,7 +662,7 @@ public function testExecuteWithNewCustomer() 'id' => $addressId, ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $formMock */ + /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $attributeMock */ $attributeMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') ->disableOriginalConstructor() ->getMock(); @@ -588,9 +674,13 @@ public function testExecuteWithNewCustomer() ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->exactly(3)) + $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturn($postValue); + ->willReturnMap([ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ['address/' . $addressId, null, $postValue['address'][$addressId]], + ]); $this->requestMock->expects($this->exactly(3)) ->method('getPost') ->willReturnMap( @@ -601,69 +691,81 @@ public function testExecuteWithNewCustomer() ] ); - /** @var \Magento\Customer\Model\Metadata\Form|\PHPUnit_Framework_MockObject_MockObject $formMock */ - $formMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ + $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') + ->disableOriginalConstructor() + ->getMock(); + $objectMock->expects($this->exactly(2)) + ->method('getData') + ->willReturnMap( + [ + ['customer', null, $postValue['customer']], + ['address/' . $addressId, null, $postValue['address'][$addressId]], + ] + ); + + $this->objectFactoryMock->expects($this->exactly(2)) + ->method('create') + ->with(['data' => $postValue]) + ->willReturn($objectMock); + + /** @var Form|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ + $customerFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + ->disableOriginalConstructor() + ->getMock(); + $customerFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'customer') + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('compactData') + ->with($dataToCompact) + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('getAttributes') + ->willReturn($attributes); + + $customerAddressFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') ->disableOriginalConstructor() ->getMock(); + $customerAddressFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'address/' . $addressId) + ->willReturn($addressFilteredData); + $customerAddressFormMock->expects($this->once()) + ->method('compactData') + ->with($addressDataToCompact) + ->willReturn($addressFilteredData); + $customerAddressFormMock->expects($this->once()) + ->method('getAttributes') + ->willReturn($attributes); $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturnMap( [ [ - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'adminhtml_customer', [], false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE, + Form::DONT_IGNORE_INVISIBLE, [], - $formMock + $customerFormMock ], [ - \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + AddressMetadataInterface::ENTITY_TYPE_ADDRESS, 'adminhtml_customer_address', [], false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE, + Form::DONT_IGNORE_INVISIBLE, [], - $formMock + $customerAddressFormMock ], ] ); - $formMock->expects($this->exactly(2)) - ->method('extractData') - ->willReturnMap( - [ - [$this->requestMock, 'customer', true, $filteredData], - [$this->requestMock, 'address/' . $addressId, true, $addressFilteredData], - ] - ); - - /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ - $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') - ->disableOriginalConstructor() - ->getMock(); - - $this->objectFactoryMock->expects($this->exactly(2)) - ->method('create') - ->with(['data' => $postValue]) - ->willReturn($objectMock); - - $objectMock->expects($this->exactly(2)) - ->method('getData') - ->willReturnMap( - [ - ['customer', null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], - ] - ); - - $formMock->expects($this->exactly(2)) - ->method('getAttributes') - ->willReturn($attributes); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ + /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ $customerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->disableOriginalConstructor() ->getMock(); @@ -680,6 +782,16 @@ public function testExecuteWithNewCustomer() ->method('create') ->willReturn($addressMock); + $this->customerAddressRepositoryMock->expects($this->once()) + ->method('getById') + ->with($addressId) + ->willReturn($addressMock); + + $this->customerAddressMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($addressMock) + ->willReturn([]); + $this->dataHelperMock->expects($this->exactly(2)) ->method('populateWithArray') ->willReturnMap( @@ -734,7 +846,7 @@ public function testExecuteWithNewCustomer() $this->registryMock->expects($this->once()) ->method('register') - ->with(\Magento\Customer\Controller\RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + ->with(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); $this->messageManagerMock->expects($this->once()) ->method('addSuccess') @@ -781,8 +893,15 @@ public function testExecuteWithNewCustomerAndValidationException() 'coolness' => false, 'disable_auto_group_change' => 'false', ]; + $dataToCompact = [ + 'disable_auto_group_change' => 'false', + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + 'confirmation' => false, + 'sendemail_store_id' => false, + ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $formMock */ + /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $attributeMock */ $attributeMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') ->disableOriginalConstructor() ->getMock(); @@ -794,9 +913,12 @@ public function testExecuteWithNewCustomerAndValidationException() ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturn($postValue); + ->willReturnMap([ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ]); $this->requestMock->expects($this->exactly(2)) ->method('getPost') ->willReturnMap( @@ -806,46 +928,47 @@ public function testExecuteWithNewCustomerAndValidationException() ] ); - /** @var \Magento\Customer\Model\Metadata\Form|\PHPUnit_Framework_MockObject_MockObject $formMock */ - $formMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') - ->disableOriginalConstructor() - ->getMock(); - - $this->formFactoryMock->expects($this->once()) - ->method('create') - ->with( - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - 'adminhtml_customer', - [], - false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE - )->willReturn($formMock); - - $formMock->expects($this->once()) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($filteredData); - /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') ->disableOriginalConstructor() ->getMock(); + $objectMock->expects($this->once()) + ->method('getData') + ->with('customer') + ->willReturn($postValue['customer']); $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); - $objectMock->expects($this->once()) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - - $formMock->expects($this->once()) + /** @var Form|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ + $customerFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + ->disableOriginalConstructor() + ->getMock(); + $customerFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'customer') + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('compactData') + ->with($dataToCompact) + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ + $this->formFactoryMock->expects($this->once()) + ->method('create') + ->with( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'adminhtml_customer', + [], + false, + Form::DONT_IGNORE_INVISIBLE + )->willReturn($customerFormMock); + + /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ $customerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->disableOriginalConstructor() ->getMock(); @@ -921,8 +1044,15 @@ public function testExecuteWithNewCustomerAndLocalizedException() 'coolness' => false, 'disable_auto_group_change' => 'false', ]; + $dataToCompact = [ + 'disable_auto_group_change' => 'false', + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + 'confirmation' => false, + 'sendemail_store_id' => false, + ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $formMock */ + /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $attributeMock */ $attributeMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') ->disableOriginalConstructor() ->getMock(); @@ -934,9 +1064,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturn($postValue); + ->willReturnMap([ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ]); $this->requestMock->expects($this->exactly(2)) ->method('getPost') ->willReturnMap( @@ -946,46 +1079,47 @@ public function testExecuteWithNewCustomerAndLocalizedException() ] ); - /** @var \Magento\Customer\Model\Metadata\Form|\PHPUnit_Framework_MockObject_MockObject $formMock */ - $formMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') - ->disableOriginalConstructor() - ->getMock(); - - $this->formFactoryMock->expects($this->once()) - ->method('create') - ->with( - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - 'adminhtml_customer', - [], - false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE - )->willReturn($formMock); - - $formMock->expects($this->once()) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($filteredData); - /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') ->disableOriginalConstructor() ->getMock(); + $objectMock->expects($this->once()) + ->method('getData') + ->with('customer') + ->willReturn($postValue['customer']); $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); - $objectMock->expects($this->once()) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - - $formMock->expects($this->once()) + /** @var Form|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ + $customerFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + ->disableOriginalConstructor() + ->getMock(); + $customerFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'customer') + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('compactData') + ->with($dataToCompact) + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ + $this->formFactoryMock->expects($this->once()) + ->method('create') + ->with( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'adminhtml_customer', + [], + false, + Form::DONT_IGNORE_INVISIBLE + )->willReturn($customerFormMock); + + /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ $customerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->disableOriginalConstructor() ->getMock(); @@ -1061,8 +1195,15 @@ public function testExecuteWithNewCustomerAndException() 'coolness' => false, 'disable_auto_group_change' => 'false', ]; + $dataToCompact = [ + 'disable_auto_group_change' => 'false', + CustomerInterface::DEFAULT_BILLING => false, + CustomerInterface::DEFAULT_SHIPPING => false, + 'confirmation' => false, + 'sendemail_store_id' => false, + ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $formMock */ + /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $attributeMock */ $attributeMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') ->disableOriginalConstructor() ->getMock(); @@ -1074,9 +1215,12 @@ public function testExecuteWithNewCustomerAndException() ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturn($postValue); + ->willReturnMap([ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ]); $this->requestMock->expects($this->exactly(2)) ->method('getPost') ->willReturnMap( @@ -1086,46 +1230,47 @@ public function testExecuteWithNewCustomerAndException() ] ); - /** @var \Magento\Customer\Model\Metadata\Form|\PHPUnit_Framework_MockObject_MockObject $formMock */ - $formMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') - ->disableOriginalConstructor() - ->getMock(); - - $this->formFactoryMock->expects($this->once()) - ->method('create') - ->with( - \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - 'adminhtml_customer', - [], - false, - \Magento\Customer\Model\Metadata\Form::DONT_IGNORE_INVISIBLE - )->willReturn($formMock); - - $formMock->expects($this->once()) - ->method('extractData') - ->with($this->requestMock, 'customer') - ->willReturn($filteredData); - /** @var \Magento\Framework\DataObject|\PHPUnit_Framework_MockObject_MockObject $objectMock */ $objectMock = $this->getMockBuilder('Magento\Framework\DataObject') ->disableOriginalConstructor() ->getMock(); + $objectMock->expects($this->once()) + ->method('getData') + ->with('customer') + ->willReturn($postValue['customer']); $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); - $objectMock->expects($this->once()) - ->method('getData') - ->with('customer') - ->willReturn($postValue['customer']); - - $formMock->expects($this->once()) + /** @var Form|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ + $customerFormMock = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form') + ->disableOriginalConstructor() + ->getMock(); + $customerFormMock->expects($this->once()) + ->method('extractData') + ->with($this->requestMock, 'customer') + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) + ->method('compactData') + ->with($dataToCompact) + ->willReturn($filteredData); + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ + $this->formFactoryMock->expects($this->once()) + ->method('create') + ->with( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'adminhtml_customer', + [], + false, + Form::DONT_IGNORE_INVISIBLE + )->willReturn($customerFormMock); + + /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ $customerMock = $this->getMockBuilder('Magento\Customer\Api\Data\CustomerInterface') ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php index cc8ed6c771806..fc6684bd425b7 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php @@ -169,10 +169,6 @@ public function testExecute() ->method('validateData') ->willReturn([$error]); - $this->extensibleDataObjectConverter->expects($this->once()) - ->method('toFlatArray') - ->willReturn([]); - $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], @@ -208,10 +204,6 @@ public function testExecuteWithoutAddresses() ->method('validateData') ->willReturn([$error]); - $this->extensibleDataObjectConverter->expects($this->once()) - ->method('toFlatArray') - ->willReturn([]); - $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], @@ -245,10 +237,6 @@ public function testExecuteWithException() $this->form->expects($this->never()) ->method('validateData'); - $this->extensibleDataObjectConverter->expects($this->once()) - ->method('toFlatArray') - ->willReturn([]); - $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php index cc98e33edb8af..29c7784f3910e 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php @@ -169,7 +169,7 @@ public function testExecuteGetParamImage() ->willReturnMap([['file', null, null], ['image', null, $decodedFile]]); $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($fileName)->willReturn($path); - $this->directoryMock->expects($this->once())->method('stat')->with($path)->willReturn($stat); + $this->directoryMock->expects($this->once())->method('stat')->with($fileName)->willReturn($stat); $this->fileSystemMock->expects($this->once())->method('getDirectoryRead') ->with(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA) diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 20ab96d73839a..85563422c7046 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Test\Unit\Model\Customer; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -38,6 +39,16 @@ class DataProviderTest extends \PHPUnit_Framework_TestCase */ protected $eavValidationRulesMock; + /** + * @var \Magento\Customer\Model\FileProcessorFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fileProcessorFactory; + + /** + * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fileProcessor; + /** * Set up * @@ -59,6 +70,15 @@ protected function setUp() ->getMockBuilder('Magento\Ui\DataProvider\EavValidationRules') ->disableOriginalConstructor() ->getMock(); + + $this->fileProcessor = $this->getMockBuilder('Magento\Customer\Model\FileProcessor') + ->disableOriginalConstructor() + ->getMock(); + + $this->fileProcessorFactory = $this->getMockBuilder('Magento\Customer\Model\FileProcessorFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); } /** @@ -280,6 +300,10 @@ public function testGetData() $customer->expects($this->once()) ->method('getAddresses') ->willReturn([$address]); + $customer->expects($this->once()) + ->method('getAttributes') + ->willReturn([]); + $address->expects($this->atLeastOnce()) ->method('getId') ->willReturn(2); @@ -294,6 +318,9 @@ public function testGetData() 'lastname' => 'lastname', 'street' => "street\nstreet", ]); + $address->expects($this->once()) + ->method('getAttributes') + ->willReturn([]); $helper = new ObjectManager($this); $dataProvider = $helper->getObject( @@ -332,4 +359,196 @@ public function testGetData() $dataProvider->getData() ); } + + public function testGetDataWithCustomAttributeImage() + { + $customerId = 1; + $customerEmail = 'user1@example.com'; + + $filename = '/filename.ext1'; + $viewUrl = 'viewUrl'; + + $expectedData = [ + $customerId => [ + 'customer' => [ + 'email' => $customerEmail, + 'img1' => [ + 'file' => $filename, + 'type' => 'image', + 'url' => $viewUrl, + ], + ], + ], + ]; + + $attributeMock = $this->getMockBuilder('Magento\Customer\Model\Attribute') + ->disableOriginalConstructor() + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getFrontendInput') + ->willReturn('image'); + $attributeMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn('img1'); + + $entityTypeMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Type') + ->disableOriginalConstructor() + ->getMock(); + $entityTypeMock->expects($this->once()) + ->method('getEntityTypeCode') + ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $customerMock = $this->getMockBuilder('Magento\Customer\Model\Customer') + ->disableOriginalConstructor() + ->getMock(); + $customerMock->expects($this->once()) + ->method('getData') + ->willReturn([ + 'email' => $customerEmail, + 'img1' => $filename, + ]); + $customerMock->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); + $customerMock->expects($this->once()) + ->method('getId') + ->willReturn($customerId); + $customerMock->expects($this->once()) + ->method('getAttributes') + ->willReturn([$attributeMock]); + $customerMock->expects($this->once()) + ->method('getEntityType') + ->willReturn($entityTypeMock); + + $collectionMock = $this->getMockBuilder('Magento\Customer\Model\ResourceModel\Customer\Collection') + ->disableOriginalConstructor() + ->getMock(); + $collectionMock->expects($this->once()) + ->method('getItems') + ->willReturn([$customerMock]); + + $this->customerCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + + $this->fileProcessorFactory->expects($this->any()) + ->method('create') + ->with([ + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]) + ->willReturn($this->fileProcessor); + + $this->fileProcessor->expects($this->once()) + ->method('isExist') + ->with($filename) + ->willReturn(true); + $this->fileProcessor->expects($this->once()) + ->method('getViewUrl') + ->with('/filename.ext1', 'image') + ->willReturn($viewUrl); + + $objectManager = new ObjectManager($this); + $dataProvider = $objectManager->getObject( + '\Magento\Customer\Model\Customer\DataProvider', + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'eavValidationRules' => $this->eavValidationRulesMock, + 'customerCollectionFactory' => $this->customerCollectionFactoryMock, + 'eavConfig' => $this->getEavConfigMock() + ] + ); + + $reflection = new \ReflectionClass(get_class($dataProvider)); + $reflectionProperty = $reflection->getProperty('fileProcessorFactory'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($dataProvider, $this->fileProcessorFactory); + + $this->assertEquals($expectedData, $dataProvider->getData()); + } + + public function testGetDataWithCustomAttributeImageNoData() + { + $customerId = 1; + $customerEmail = 'user1@example.com'; + + $expectedData = [ + $customerId => [ + 'customer' => [ + 'email' => $customerEmail, + 'img1' => [], + ], + ], + ]; + + $attributeMock = $this->getMockBuilder('Magento\Customer\Model\Attribute') + ->disableOriginalConstructor() + ->getMock(); + $attributeMock->expects($this->once()) + ->method('getFrontendInput') + ->willReturn('image'); + $attributeMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn('img1'); + + $entityTypeMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Type') + ->disableOriginalConstructor() + ->getMock(); + $entityTypeMock->expects($this->once()) + ->method('getEntityTypeCode') + ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $customerMock = $this->getMockBuilder('Magento\Customer\Model\Customer') + ->disableOriginalConstructor() + ->getMock(); + $customerMock->expects($this->once()) + ->method('getData') + ->willReturn([ + 'email' => $customerEmail, + ]); + $customerMock->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); + $customerMock->expects($this->once()) + ->method('getId') + ->willReturn($customerId); + $customerMock->expects($this->once()) + ->method('getAttributes') + ->willReturn([$attributeMock]); + $customerMock->expects($this->once()) + ->method('getEntityType') + ->willReturn($entityTypeMock); + + $collectionMock = $this->getMockBuilder('Magento\Customer\Model\ResourceModel\Customer\Collection') + ->disableOriginalConstructor() + ->getMock(); + $collectionMock->expects($this->once()) + ->method('getItems') + ->willReturn([$customerMock]); + + $this->customerCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + + $objectManager = new ObjectManager($this); + $dataProvider = $objectManager->getObject( + '\Magento\Customer\Model\Customer\DataProvider', + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'eavValidationRules' => $this->eavValidationRulesMock, + 'customerCollectionFactory' => $this->customerCollectionFactoryMock, + 'eavConfig' => $this->getEavConfigMock() + ] + ); + + $reflection = new \ReflectionClass(get_class($dataProvider)); + $reflectionProperty = $reflection->getProperty('fileProcessorFactory'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($dataProvider, $this->fileProcessorFactory); + + $this->assertEquals($expectedData, $dataProvider->getData()); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php index 2073eb5de3a58..f441f0c86dc6e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php @@ -120,6 +120,10 @@ public function testExtract() ->method('extractData') ->with($this->request) ->willReturn($customerData); + $this->customerForm->expects($this->once()) + ->method('compactData') + ->with($customerData) + ->willReturn($customerData); $this->customerForm->expects($this->once()) ->method('getAllowedAttributes') ->willReturn(['group_id' => 'attribute object']); diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php new file mode 100644 index 0000000000000..ef36561d2ff66 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -0,0 +1,135 @@ +mediaDirectory = $this->getMockBuilder('Magento\Framework\Filesystem\Directory\WriteInterface') + ->getMockForAbstractClass(); + + $this->filesystem = $this->getMockBuilder('Magento\Framework\Filesystem') + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem->expects($this->any()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectory); + + $this->urlBuilder = $this->getMockBuilder('Magento\Framework\UrlInterface') + ->getMockForAbstractClass(); + + $this->urlEncoder = $this->getMockBuilder('Magento\Framework\Url\EncoderInterface') + ->getMockForAbstractClass(); + } + + /** + * @param string $entityTypeCode + * @return FileProcessor + */ + private function getModel($entityTypeCode) + { + $model = new FileProcessor( + $this->filesystem, + $this->urlBuilder, + $this->urlEncoder, + $entityTypeCode + ); + + return $model; + } + + public function testIsExist() + { + $fileName = '/filename.ext1'; + + $this->mediaDirectory->expects($this->once()) + ->method('isExist') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . $fileName) + ->willReturn(true); + + $this->model = new FileProcessor( + $this->filesystem, + $this->urlBuilder, + $this->urlEncoder, + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER + ); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $this->assertTrue($model->isExist($fileName)); + } + + public function testGetViewUrlCustomer() + { + $filePath = 'filename.ext1'; + $encodedFilePath = 'encodedfilenameext1'; + + $fileUrl = 'fileUrl'; + + $this->urlEncoder->expects($this->once()) + ->method('encode') + ->with($filePath) + ->willReturn($encodedFilePath); + + $this->urlBuilder->expects($this->once()) + ->method('getUrl') + ->with('customer/index/viewfile', ['image' => $encodedFilePath]) + ->willReturn($fileUrl); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $this->assertEquals($fileUrl, $model->getViewUrl($filePath, 'image')); + } + + public function testGetViewUrlCustomerAddress() + { + $filePath = 'filename.ext1'; + + $baseUrl = 'baseUrl'; + $relativeUrl = 'relativeUrl'; + + $this->urlBuilder->expects($this->once()) + ->method('getBaseUrl') + ->with(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]) + ->willReturn($baseUrl); + + $this->mediaDirectory->expects($this->once()) + ->method('getRelativePath') + ->with(AddressMetadataInterface::ENTITY_TYPE_ADDRESS . '/' . $filePath) + ->willReturn($relativeUrl); + + $model = $this->getModel(AddressMetadataInterface::ENTITY_TYPE_ADDRESS); + + $this->assertEquals($baseUrl . $relativeUrl, $model->getViewUrl($filePath, 'image')); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php index 7a5b08fa531cb..179d49a441bce 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Test\Unit\Model\Metadata\Form; use Magento\Customer\Model\Metadata\ElementFactory; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; class FileTest extends AbstractFormTestCase @@ -30,6 +31,11 @@ class FileTest extends AbstractFormTestCase */ protected $uploaderFactoryMock; + /** + * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileProcessorMock; + protected function setUp() { parent::setUp(); @@ -43,19 +49,21 @@ protected function setUp() $this->requestMock = $this->getMockBuilder('Magento\Framework\App\Request\Http') ->disableOriginalConstructor()->getMock(); $this->uploaderFactoryMock = $this->getMock('Magento\Framework\File\UploaderFactory', [], [], '', false); + + $this->fileProcessorMock = $this->getMockBuilder('Magento\Customer\Model\FileProcessor') + ->disableOriginalConstructor() + ->getMock(); } /** * @param array|bool $expected * @param string $attributeCode - * @param bool $isAjax * @param string $delete * @dataProvider extractValueNoRequestScopeDataProvider */ - public function testExtractValueNoRequestScope($expected, $attributeCode = '', $isAjax = false, $delete = '') + public function testExtractValueNoRequestScope($expected, $attributeCode = '', $delete = '') { $value = 'value'; - $fileForm = $this->getClass($value, $isAjax); $this->requestMock->expects( $this->any() @@ -75,7 +83,14 @@ public function testExtractValueNoRequestScope($expected, $attributeCode = '', $ if (!empty($attributeCode)) { $_FILES[$attributeCode] = ['attributeCodeValue']; } - $this->assertEquals($expected, $fileForm->extractValue($this->requestMock)); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($expected, $model->extractValue($this->requestMock)); if (!empty($attributeCode)) { unset($_FILES[$attributeCode]); } @@ -84,11 +99,10 @@ public function testExtractValueNoRequestScope($expected, $attributeCode = '', $ public function extractValueNoRequestScopeDataProvider() { return [ - 'ajax' => [false, '', true], 'no_file' => [[]], - 'delete' => [['delete' => true], '', false, true], - 'file_delete' => [['attributeCodeValue', 'delete' => true], 'attributeCode', false, true], - 'file_!delete' => [['attributeCodeValue'], 'attributeCode', false, false] + 'delete' => [['delete' => true], '', true], + 'file_delete' => [['attributeCodeValue', 'delete' => true], 'attributeCode', true], + 'file_!delete' => [['attributeCodeValue'], 'attributeCode', false] ]; } @@ -101,7 +115,6 @@ public function extractValueNoRequestScopeDataProvider() public function testExtractValueWithRequestScope($expected, $requestScope, $mainScope = false) { $value = 'value'; - $fileForm = $this->getClass($value, false); $this->requestMock->expects( $this->any() @@ -126,12 +139,18 @@ public function testExtractValueWithRequestScope($expected, $requestScope, $main $this->returnValue('attributeCode') ); - $fileForm->setRequestScope($requestScope); + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $model->setRequestScope($requestScope); if ($mainScope) { $_FILES['mainScope'] = $mainScope; } - $this->assertEquals($expected, $fileForm->extractValue($this->requestMock)); + $this->assertEquals($expected, $model->extractValue($this->requestMock)); if ($mainScope) { unset($_FILES['mainScope']); } @@ -163,7 +182,6 @@ public function extractValueWithRequestScopeDataProvider() */ public function testValidateValueNotToUpload($expected, $value, $isAjax = false, $isRequired = true) { - $fileForm = $this->getClass($value, $isAjax); $this->attributeMetadataMock->expects( $this->any() )->method( @@ -179,7 +197,13 @@ public function testValidateValueNotToUpload($expected, $value, $isAjax = false, $this->returnValue('attributeLabel') ); - $this->assertEquals($expected, $fileForm->validateValue($value)); + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => $isAjax, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($expected, $model->validateValue($value)); } public function validateValueNotToUploadDataProvider() @@ -201,8 +225,7 @@ public function validateValueNotToUploadDataProvider() public function testValidateValueToUpload($expected, $value, $parameters = []) { $parameters = array_merge(['uploaded' => true, 'valid' => true], $parameters); - $fileForm = $this->getClass($value, false); - $fileForm->expects($this->any())->method('_isUploadedFile')->will($this->returnValue($parameters['uploaded'])); + $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); $this->attributeMetadataMock->expects( $this->any() @@ -226,7 +249,18 @@ public function testValidateValueToUpload($expected, $value, $parameters = []) )->will( $this->returnValue($parameters['valid']) ); - $this->assertEquals($expected, $fileForm->validateValue($value)); + + $this->fileProcessorMock->expects($this->any()) + ->method('isExist') + ->willReturn($parameters['uploaded']); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($expected, $model->validateValue($value)); } public function validateValueToUploadDataProvider() @@ -242,36 +276,53 @@ public function validateValueToUploadDataProvider() ['tmp_name' => 'tempName_0001.bin', 'name' => 'realFileName.bin'], ['uploaded' => false], ], - 'isValid' => [true, ['tmp_name' => 'tempName_0001.txt', 'name' => 'realFileName.txt']] ]; } public function testCompactValueIsAjax() { - $fileForm = $this->getClass('value', true); - $this->assertSame($fileForm, $fileForm->compactValue('aValue')); + $model = $this->initialize([ + 'value' => 'value', + 'isAjax' => true, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame($model, $model->compactValue('aValue')); } public function testCompactValueNoDelete() { - $fileForm = $this->getClass('value', false); $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); - $this->assertSame('value', $fileForm->compactValue([])); + + $model = $this->initialize([ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame('value', $model->compactValue([])); } public function testCompactValueDelete() { - $fileForm = $this->getClass('value', false); $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); + $mediaDirMock = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\WriteInterface'); $mediaDirMock->expects($this->once()) ->method('delete') - ->with(self::ENTITY_TYPE . 'value'); + ->with(self::ENTITY_TYPE . '/' . 'value'); $this->fileSystemMock->expects($this->once()) ->method('getDirectoryWrite') - ->with(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA) + ->with(DirectoryList::MEDIA) ->will($this->returnValue($mediaDirMock)); - $this->assertSame('', $fileForm->compactValue(['delete' => true])); + + $model = $this->initialize([ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame('', $model->compactValue(['delete' => true])); } public function testCompactValueTmpFile() @@ -279,11 +330,10 @@ public function testCompactValueTmpFile() $value = ['tmp_name' => 'tmp.file', 'name' => 'new.file']; $expected = 'saved.file'; - $fileForm = $this->getClass(null, false); $mediaDirMock = $this->getMockForAbstractClass('\Magento\Framework\Filesystem\Directory\WriteInterface'); $this->fileSystemMock->expects($this->once()) ->method('getDirectoryWrite') - ->with(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA) + ->with(DirectoryList::MEDIA) ->will($this->returnValue($mediaDirMock)); $mediaDirMock->expects($this->any()) ->method('getAbsolutePath') @@ -309,14 +359,26 @@ public function testCompactValueTmpFile() ->method('getUploadedFileName') ->will($this->returnValue($expected)); - $this->assertSame($expected, $fileForm->compactValue($value)); + $model = $this->initialize([ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame($expected, $model->compactValue($value)); } public function testRestoreValue() { $value = 'value'; - $fileForm = $this->getClass($value, false); - $this->assertEquals($value, $fileForm->restoreValue('aValue')); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($value, $model->restoreValue('aValue')); } /** @@ -325,8 +387,13 @@ public function testRestoreValue() */ public function testOutputValueNonJson($format) { - $fileForm = $this->getClass('value', false); - $this->assertSame('', $fileForm->outputValue($format)); + $model = $this->initialize([ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame('', $model->outputValue($format)); } public function outputValueDataProvider() @@ -344,7 +411,7 @@ public function testOutputValueJson() { $value = 'value'; $urlKey = 'url_key'; - $fileForm = $this->getClass($value, false); + $this->urlEncode->expects( $this->once() )->method( @@ -354,36 +421,151 @@ public function testOutputValueJson() )->will( $this->returnValue($urlKey) ); + $expected = ['value' => $value, 'url_key' => $urlKey]; - $this->assertSame($expected, $fileForm->outputValue(ElementFactory::OUTPUT_FORMAT_JSON)); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertSame($expected, $model->outputValue(ElementFactory::OUTPUT_FORMAT_JSON)); + } + + public function testCompactValueNoAction() + { + $value = 'value'; + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($value, $model->compactValue($value)); + } + + public function testCompactValueInputField() + { + $value = [ + 'name' => 'filename.ext1', + 'tmp_name' => 'tmpfilename.ext1', + ]; + + $absolutePath = 'absolute_path'; + $uploadedFilename = 'filename.ext1'; + + $mediaDirectoryMock = $this->getMockBuilder('Magento\Framework\Filesystem\Directory\WriteInterface') + ->getMockForAbstractClass(); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with(self::ENTITY_TYPE) + ->willReturn($absolutePath); + + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + + $uploaderMock = $this->getMockBuilder('Magento\Framework\File\Uploader') + ->disableOriginalConstructor() + ->getMock(); + $uploaderMock->expects($this->once()) + ->method('setFilesDispersion') + ->with(true) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setFilenamesCaseSensitivity') + ->with(false) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setAllowRenameFiles') + ->with(true) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('save') + ->with($absolutePath, $value['name']) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('getUploadedFileName') + ->willReturn($uploadedFilename); + + $this->uploaderFactoryMock->expects($this->once()) + ->method('create') + ->with(['fileId' => $value]) + ->willReturn($uploaderMock); + + $model = $this->initialize([ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals($uploadedFilename, $model->compactValue($value)); + } + + public function testCompactValueInputFieldWithException() + { + $value = [ + 'name' => 'filename.ext1', + 'tmp_name' => 'tmpfilename.ext1', + ]; + + $originValue = 'origin'; + + $mediaDirectoryMock = $this->getMockBuilder('Magento\Framework\Filesystem\Directory\WriteInterface') + ->getMockForAbstractClass(); + $mediaDirectoryMock->expects($this->once()) + ->method('delete') + ->with(self::ENTITY_TYPE . '/' . $originValue); + + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + + $exception = new \Exception('Error'); + + $this->uploaderFactoryMock->expects($this->once()) + ->method('create') + ->with(['fileId' => $value]) + ->willThrowException($exception); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($exception) + ->willReturnSelf(); + + $model = $this->initialize([ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->assertEquals('', $model->compactValue($value)); } /** - * Helper for creating the unit under test. - * - * @param string|int|bool|null $value The value undergoing testing by a given test - * @param bool $isAjax - * @return \PHPUnit_Framework_MockObject_MockObject | File + * @param array $data + * @return \Magento\Customer\Model\Metadata\Form\File */ - protected function getClass($value, $isAjax) + private function initialize(array $data) { - $fileForm = $this->getMock( - 'Magento\Customer\Model\Metadata\Form\File', - ['_isUploadedFile'], - [ - $this->localeMock, - $this->loggerMock, - $this->attributeMetadataMock, - $this->localeResolverMock, - $value, - self::ENTITY_TYPE, - $isAjax, - $this->urlEncode, - $this->fileValidatorMock, - $this->fileSystemMock, - $this->uploaderFactoryMock - ] + $model = new \Magento\Customer\Model\Metadata\Form\File( + $this->localeMock, + $this->loggerMock, + $this->attributeMetadataMock, + $this->localeResolverMock, + $data['value'], + $data['entityTypeCode'], + $data['isAjax'], + $this->urlEncode, + $this->fileValidatorMock, + $this->fileSystemMock, + $this->uploaderFactoryMock ); - return $fileForm; + + return $model; } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php index f002a5cb1e4a5..6daf1543e5507 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php @@ -5,20 +5,62 @@ */ namespace Magento\Customer\Test\Unit\Model\Metadata\Form; -use Magento\Customer\Model\Metadata\Form\Image; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\FileProcessor; -class ImageTest extends FileTest +class ImageTest extends AbstractFormTestCase { /** - * Create an instance of the class that is being tested - * - * @param string|int|bool|null $value - * @param bool $isAjax - * @return Image + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Url\EncoderInterface */ - protected function getClass($value, $isAjax) + protected $urlEncode; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\MediaStorage\Model\File\Validator\NotProtectedExtension + */ + protected $fileValidatorMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Filesystem + */ + protected $fileSystemMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\File\UploaderFactory + */ + protected $uploaderFactoryMock; + + protected function setUp() { - $imageForm = $this->getMock( + parent::setUp(); + + $this->urlEncode = $this->getMockBuilder('Magento\Framework\Url\EncoderInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->fileValidatorMock = $this->getMockBuilder( + 'Magento\MediaStorage\Model\File\Validator\NotProtectedExtension' + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->fileSystemMock = $this->getMockBuilder('Magento\Framework\Filesystem') + ->disableOriginalConstructor() + ->getMock(); + + $this->uploaderFactoryMock = $this->getMockBuilder('Magento\Framework\File\UploaderFactory') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @param array $data + * @return \Magento\Customer\Model\Metadata\Form\File + */ + private function initialize(array $data) + { + $model = $this->getMock( 'Magento\Customer\Model\Metadata\Form\Image', ['_isUploadedFile'], [ @@ -26,16 +68,61 @@ protected function getClass($value, $isAjax) $this->loggerMock, $this->attributeMetadataMock, $this->localeResolverMock, - $value, - 0, - $isAjax, + $data['value'], + $data['entityTypeCode'], + $data['isAjax'], $this->urlEncode, $this->fileValidatorMock, $this->fileSystemMock, $this->uploaderFactoryMock ] ); - return $imageForm; + + return $model; + } + + public function testValidateIsNotValidFile() + { + $value = [ + 'tmp_name' => 'tmp_file', + 'name' => 'realFileName', + ]; + + $this->attributeMetadataMock->expects($this->once()) + ->method('getStoreLabel') + ->willReturn('File Input Field Label'); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $this->assertEquals(['"realFileName" is not a valid file.'], $model->validateValue($value)); + } + + public function testValidate() + { + $value = [ + 'tmp_name' => __DIR__ . '/_files/logo.gif', + 'name' => 'logo.gif', + ]; + + $this->attributeMetadataMock->expects($this->once()) + ->method('getStoreLabel') + ->willReturn('File Input Field Label'); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $model->expects($this->any()) + ->method('_isUploadedFile') + ->will($this->returnValue($value['tmp_name'])); + + $this->assertTrue($model->validateValue($value)); } public function validateValueToUploadDataProvider() @@ -50,4 +137,155 @@ public function validateValueToUploadDataProvider() [true, ['tmp_name' => $imagePath, 'name' => 'logo.gif']] ]; } + + public function testCompactValueUiComponentCustomerNotExists() + { + $originValue = 'filename.ext1'; + + $value = [ + 'file' => 'filename.ext2', + 'name' => 'filename.ext2', + 'type' => 'image', + ]; + + $model = $this->initialize([ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $this->assertEquals($originValue, $model->compactValue($value)); + } + + public function testValidateMaxFileSize() + { + $value = [ + 'tmp_name' => __DIR__ . '/_files/logo.gif', + 'name' => 'logo.gif', + 'size' => 2, + ]; + + $maxFileSize = 1; + + $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') + ->getMockForAbstractClass(); + $validationRuleMock->expects($this->any()) + ->method('getName') + ->willReturn('max_file_size'); + $validationRuleMock->expects($this->any()) + ->method('getValue') + ->willReturn($maxFileSize); + + $this->attributeMetadataMock->expects($this->once()) + ->method('getStoreLabel') + ->willReturn('File Input Field Label'); + $this->attributeMetadataMock->expects($this->once()) + ->method('getValidationRules') + ->willReturn([$validationRuleMock]); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $model->expects($this->any()) + ->method('_isUploadedFile') + ->will($this->returnValue($value['tmp_name'])); + + $this->assertEquals(['"logo.gif" exceeds the allowed file size.'], $model->validateValue($value)); + } + + public function testValidateMaxImageWidth() + { + $value = [ + 'tmp_name' => __DIR__ . '/_files/logo.gif', + 'name' => 'logo.gif', + ]; + + $maxImageWidth = 1; + + $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') + ->getMockForAbstractClass(); + $validationRuleMock->expects($this->any()) + ->method('getName') + ->willReturn('max_image_width'); + $validationRuleMock->expects($this->any()) + ->method('getValue') + ->willReturn($maxImageWidth); + + $this->attributeMetadataMock->expects($this->once()) + ->method('getStoreLabel') + ->willReturn('File Input Field Label'); + $this->attributeMetadataMock->expects($this->once()) + ->method('getValidationRules') + ->willReturn([$validationRuleMock]); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $model->expects($this->any()) + ->method('_isUploadedFile') + ->will($this->returnValue($value['tmp_name'])); + + $this->assertEquals(['"logo.gif" width exceeds allowed value of 1 px.'], $model->validateValue($value)); + } + + public function testValidateMaxImageHeight() + { + $value = [ + 'tmp_name' => __DIR__ . '/_files/logo.gif', + 'name' => 'logo.gif', + ]; + + $maxImageHeight = 1; + + $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') + ->getMockForAbstractClass(); + $validationRuleMock->expects($this->any()) + ->method('getName') + ->willReturn('max_image_heght'); + $validationRuleMock->expects($this->any()) + ->method('getValue') + ->willReturn($maxImageHeight); + + $this->attributeMetadataMock->expects($this->once()) + ->method('getStoreLabel') + ->willReturn('File Input Field Label'); + $this->attributeMetadataMock->expects($this->once()) + ->method('getValidationRules') + ->willReturn([$validationRuleMock]); + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $model->expects($this->any()) + ->method('_isUploadedFile') + ->will($this->returnValue($value['tmp_name'])); + + $this->assertEquals(['"logo.gif" height exceeds allowed value of 1 px.'], $model->validateValue($value)); + } + + public function testCompactValueNoChanges() + { + $originValue = 'filename.ext1'; + + $value = [ + 'file' => $originValue, + ]; + + $model = $this->initialize([ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $this->assertEquals($originValue, $model->compactValue($value)); + } } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php index 264b948f34b89..a6795b6a2cfe1 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php @@ -5,6 +5,10 @@ */ namespace Magento\Ui\Component\Form\Element\DataType; +use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentInterface; + /** * Class Media */ @@ -12,6 +16,28 @@ class Media extends AbstractDataType { const NAME = 'media'; + /** + * @var Repository + */ + private $assetRepo; + + /** + * @param ContextInterface $context + * @param Repository $assetRepo + * @param UiComponentInterface[] $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + Repository $assetRepo, + array $components = [], + array $data = [] + ) { + $this->assetRepo = $assetRepo; + + parent::__construct($context, $components, $data); + } + /** * Get component name * @@ -21,4 +47,20 @@ public function getComponentName() { return static::NAME; } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + $config = $this->getData('config'); + + $config['placeholder'] = $this->assetRepo->getUrl('images/fam_bullet_disk.gif'); + + $this->setData('config', $config); + + parent::prepare(); + } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index f8c27b55120cc..ab2fc78185324 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -10,9 +10,16 @@ define([ return Abstract.extend({ defaults: { + deleteCheckbox: false, links: { - value: '' - } + value: '', + file: '${ $.provider }:${ $.dataScope }.file', + type: '${ $.provider }:${ $.dataScope }.type', + url: '${ $.provider }:${ $.dataScope }.url', + deleteCheckbox: '${ $.provider }:${ $.dataScope }.delete' + }, + width: 22, + height: 22 }, /** @@ -27,6 +34,19 @@ define([ return this; }, + /** + * Initializes observable properties of instance + * + * @returns {Abstract} Chainable. + */ + initObservable: function () { + this._super(); + + this.observe('deleteCheckbox'); + + return this; + }, + /** * Defines form ID with which file input will be associated. * @@ -43,6 +63,13 @@ define([ this.formId = namespace[0]; return this; + }, + + /** + * Calls global image preview handler + */ + callPreviewHandler: function() { + imagePreview('image-' + this.uid); } }); }); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/media.html b/app/code/Magento/Ui/view/base/web/templates/form/element/media.html index 18b1b0d4be903..e0198ae77f45a 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/media.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/media.html @@ -4,6 +4,39 @@ * See COPYING.txt for license details. */ --> + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + From 9473abecda696c17f4f6d6c3f89b1f2c01a9d132 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 17:44:36 +0300 Subject: [PATCH 11/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Model/Customer/DataProvider.php | 3 ++ .../Unit/Model/Customer/DataProviderTest.php | 38 ++++++++++--------- .../Ui/view/base/web/js/form/element/media.js | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 1e7d359400baf..98833a689bed6 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -20,6 +20,7 @@ /** * Class DataProvider + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider { @@ -100,6 +101,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param FileProcessorFactory $fileProcessorFactory * @param array $meta * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -169,6 +171,7 @@ public function getData() * * @param Customer|Address $entity * @param array $entityData + * @return void */ private function overrideFileUploaderData($entity, array &$entityData) { diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 85563422c7046..be09128d75717 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -364,10 +364,8 @@ public function testGetDataWithCustomAttributeImage() { $customerId = 1; $customerEmail = 'user1@example.com'; - $filename = '/filename.ext1'; $viewUrl = 'viewUrl'; - $expectedData = [ $customerId => [ 'customer' => [ @@ -403,10 +401,7 @@ public function testGetDataWithCustomAttributeImage() ->getMock(); $customerMock->expects($this->once()) ->method('getData') - ->willReturn([ - 'email' => $customerEmail, - 'img1' => $filename, - ]); + ->willReturn(['email' => $customerEmail, 'img1' => $filename]); $customerMock->expects($this->once()) ->method('getAddresses') ->willReturn([]); @@ -433,9 +428,7 @@ public function testGetDataWithCustomAttributeImage() $this->fileProcessorFactory->expects($this->any()) ->method('create') - ->with([ - 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - ]) + ->with(['entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER]) ->willReturn($this->fileProcessor); $this->fileProcessor->expects($this->once()) @@ -459,11 +452,7 @@ public function testGetDataWithCustomAttributeImage() 'eavConfig' => $this->getEavConfigMock() ] ); - - $reflection = new \ReflectionClass(get_class($dataProvider)); - $reflectionProperty = $reflection->getProperty('fileProcessorFactory'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($dataProvider, $this->fileProcessorFactory); + $this->setBackwardCompatibleProperty($dataProvider, 'fileProcessorFactory', $this->fileProcessorFactory); $this->assertEquals($expectedData, $dataProvider->getData()); } @@ -544,11 +533,24 @@ public function testGetDataWithCustomAttributeImageNoData() ] ); - $reflection = new \ReflectionClass(get_class($dataProvider)); - $reflectionProperty = $reflection->getProperty('fileProcessorFactory'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($dataProvider, $this->fileProcessorFactory); + $this->setBackwardCompatibleProperty($dataProvider, 'fileProcessorFactory', $this->fileProcessorFactory); $this->assertEquals($expectedData, $dataProvider->getData()); } + + /** + * Set mocked property + * + * @param object $object + * @param string $propertyName + * @param object $propertyValue + * @return void + */ + public function setBackwardCompatibleProperty($object, $propertyName, $propertyValue) + { + $reflection = new \ReflectionClass(get_class($object)); + $reflectionProperty = $reflection->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $propertyValue); + } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index ab2fc78185324..af21aee7f1f6a 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -68,7 +68,7 @@ define([ /** * Calls global image preview handler */ - callPreviewHandler: function() { + callPreviewHandler: function () { imagePreview('image-' + this.uid); } }); From 39f5d8c1c42a8ecd27883ab7bc1fc910ee04bf7b Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 18:06:27 +0300 Subject: [PATCH 12/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- app/code/Magento/Customer/Controller/Account/EditPost.php | 2 +- app/code/Magento/Customer/Controller/Address/FormPost.php | 2 +- app/code/Magento/Customer/Model/Customer/DataProvider.php | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index a3891710ae5a7..b7ae1b5b6df16 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -204,7 +204,7 @@ protected function changeCustomerPassword($email) private function getCustomerMapper() { if ($this->customerMapper === null) { - $this->customerMapper = ObjectManager::getInstance()->get('Magento\Customer\Model\Customer\Mapper'); + $this->customerMapper = ObjectManager::getInstance()->get(Mapper::class); } return $this->customerMapper; } diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index aaa94eec889bf..48572fac6722f 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -227,7 +227,7 @@ public function execute() private function getCustomerAddressMapper() { if ($this->customerAddressMapper === null) { - $this->customerAddressMapper = ObjectManager::getInstance()->get('Magento\Customer\Model\Address\Mapper'); + $this->customerAddressMapper = ObjectManager::getInstance()->get(Mapper::class); } return $this->customerAddressMapper; } diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 98833a689bed6..c52e6149983d1 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -299,8 +299,7 @@ protected function prepareAddressData($addressId, array &$addresses, array $cust private function getFileProcessorFactory() { if ($this->fileProcessorFactory === null) { - $this->fileProcessorFactory = ObjectManager::getInstance() - ->get('Magento\Customer\Model\FileProcessorFactory'); + $this->fileProcessorFactory = ObjectManager::getInstance()->get(FileProcessorFactory::class); } return $this->fileProcessorFactory; } From e3a4fc2278603dafbb6cc3a784d00d9836b04a21 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 18:23:51 +0300 Subject: [PATCH 13/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- app/code/Magento/Ui/view/base/web/js/form/element/media.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index af21aee7f1f6a..6687824e4f8f1 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -69,7 +69,9 @@ define([ * Calls global image preview handler */ callPreviewHandler: function () { + /* eslint-disable no-undef */ imagePreview('image-' + this.uid); + /* eslint-enable no-undef */ } }); }); From af4a60cd2cb10f6bd66723f1a1cb47311864c47d Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 18:33:50 +0300 Subject: [PATCH 14/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Component/Form/Element/DataType/Media.php | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php index a6795b6a2cfe1..500e453f6d96b 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php @@ -5,9 +5,8 @@ */ namespace Magento\Ui\Component\Form\Element\DataType; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Asset\Repository; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\View\Element\UiComponentInterface; /** * Class Media @@ -21,23 +20,6 @@ class Media extends AbstractDataType */ private $assetRepo; - /** - * @param ContextInterface $context - * @param Repository $assetRepo - * @param UiComponentInterface[] $components - * @param array $data - */ - public function __construct( - ContextInterface $context, - Repository $assetRepo, - array $components = [], - array $data = [] - ) { - $this->assetRepo = $assetRepo; - - parent::__construct($context, $components, $data); - } - /** * Get component name * @@ -57,10 +39,25 @@ public function prepare() { $config = $this->getData('config'); - $config['placeholder'] = $this->assetRepo->getUrl('images/fam_bullet_disk.gif'); + $config['placeholder'] = $this->getAssetRepo()->getUrl('images/fam_bullet_disk.gif'); $this->setData('config', $config); parent::prepare(); } + + /** + * Get Repository instance + * + * @return Repository + * + * @deprecated + */ + private function getAssetRepo() + { + if ($this->assetRepo === null) { + $this->assetRepo = ObjectManager::getInstance()->get(Repository::class); + } + return $this->assetRepo; + } } From 5f6da58f05a5def543bbd2f337f12a75a9ada9d6 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 18:43:44 +0300 Subject: [PATCH 15/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- app/code/Magento/Ui/view/base/web/js/form/element/media.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index 6687824e4f8f1..86a7d130ad590 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -69,6 +69,7 @@ define([ * Calls global image preview handler */ callPreviewHandler: function () { + /* eslint-disable no-undef */ imagePreview('image-' + this.uid); /* eslint-enable no-undef */ From 8f35e21e8cd2b4a5c9f1db3875149a7ef5593c31 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 5 Oct 2016 19:18:26 +0300 Subject: [PATCH 16/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- app/code/Magento/Ui/view/base/web/js/form/element/media.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index 86a7d130ad590..449b6b571b631 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -72,6 +72,7 @@ define([ /* eslint-disable no-undef */ imagePreview('image-' + this.uid); + /* eslint-enable no-undef */ } }); From 81f333cfd3f22c61955ed2d92a009c6f82c2ab65 Mon Sep 17 00:00:00 2001 From: Sviatoslav Mankivskyi Date: Fri, 7 Oct 2016 12:09:57 +0300 Subject: [PATCH 17/48] MAGETWO-59461: [Backport] - [Github] magento/framework depends on zendframework/zend-stdlib but it's missing from composer.json #6442 - for 2.0 --- lib/internal/Magento/Framework/composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index fdbe5796d9fc7..65b1d72572934 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -19,7 +19,9 @@ "ext-gd": "*", "ext-openssl": "*", "lib-libxml": "*", - "ext-xsl": "*" + "ext-xsl": "*", + "zendframework/zend-stdlib": "~2.4.6", + "zendframework/zend-http": "~2.4.6" }, "suggest": { "ext-imagick": "Use Image Magick >=3.0.0 as an optional alternative image processing library" From cda473aacf36ebf7088614f8d8d46af242cb001a Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Fri, 7 Oct 2016 18:21:51 +0300 Subject: [PATCH 18/48] MAGETWO-59434: Error on adding address during checkout if add custom customer address attribute of file type --- app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index 920c38c2d3292..cfc34816718d8 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -23,6 +23,8 @@ class AttributeMerger 'textarea' => 'Magento_Ui/js/form/element/textarea', 'multiline' => 'Magento_Ui/js/form/components/group', 'multiselect' => 'Magento_Ui/js/form/element/multiselect', + 'image' => 'Magento_Ui/js/form/element/media', + 'file' => 'Magento_Ui/js/form/element/media', ]; /** @@ -32,6 +34,7 @@ class AttributeMerger */ protected $templateMap = [ 'image' => 'media', + 'file' => 'media', ]; /** From 3e6094cc7736a4c1c7a661d83d235b04d501bfc6 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Fri, 7 Oct 2016 18:38:02 +0300 Subject: [PATCH 19/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Magento/Customer/Controller/Adminhtml/IndexTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 7e86aedcc6310..b37d1cbdc6c8d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -337,7 +337,12 @@ public function testSaveActionExistingCustomerUnsubscribeNewsletter() $this->assertEquals(1, $subscriber->getStatus()); $post = [ - 'customer' => ['entity_id' => $customerId], + 'customer' => [ + 'entity_id' => $customerId, + 'email' => 'customer@example.com', + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + ], 'subscription' => 'false' ]; $this->getRequest()->setPostValue($post); From bab6c421219dccbe4cc2b9f817f4958a5ff8bd90 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Fri, 7 Oct 2016 18:51:48 +0300 Subject: [PATCH 20/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Controller/Adminhtml/Index/Validate.php | 6 +++++- .../Unit/Controller/Adminhtml/Index/ValidateTest.php | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php index 2b3cbc2d2b6a0..7c1f23881ec3d 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php @@ -28,7 +28,11 @@ protected function _validateCustomer($response) $customerForm = $this->_formFactory->create( 'customer', 'adminhtml_customer', - [], + $this->_extensibleDataObjectConverter->toFlatArray( + $customer, + [], + '\Magento\Customer\Api\Data\CustomerInterface' + ), true ); $customerForm->setInvisibleIgnored(true); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php index fc6684bd425b7..cc8ed6c771806 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php @@ -169,6 +169,10 @@ public function testExecute() ->method('validateData') ->willReturn([$error]); + $this->extensibleDataObjectConverter->expects($this->once()) + ->method('toFlatArray') + ->willReturn([]); + $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], @@ -204,6 +208,10 @@ public function testExecuteWithoutAddresses() ->method('validateData') ->willReturn([$error]); + $this->extensibleDataObjectConverter->expects($this->once()) + ->method('toFlatArray') + ->willReturn([]); + $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], @@ -237,6 +245,10 @@ public function testExecuteWithException() $this->form->expects($this->never()) ->method('validateData'); + $this->extensibleDataObjectConverter->expects($this->once()) + ->method('toFlatArray') + ->willReturn([]); + $validationResult = $this->getMockForAbstractClass( 'Magento\Customer\Api\Data\ValidationResultsInterface', [], From 2ac04a019f91623e099e08b29a3a2fd151ad7b23 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Mon, 10 Oct 2016 14:41:09 +0300 Subject: [PATCH 21/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- app/code/Magento/Sales/Model/InvoiceOrder.php | 53 ++--- .../CreditmemoValidatorInterface.php | 3 +- .../ItemCreationValidatorInterface.php | 3 +- .../Model/Order/CreditmemoDocumentFactory.php | 2 + .../Invoice/InvoiceValidatorInterface.php | 3 +- .../Model/Order/OrderValidatorInterface.php | 3 +- .../Shipment/ShipmentValidatorInterface.php | 3 +- .../Model/Order/Validation/InvoiceOrder.php | 79 +++++++ .../Validation/InvoiceOrderInterface.php | 42 ++++ .../Model/Order/Validation/RefundInvoice.php | 123 +++++++++++ .../Validation/RefundInvoiceInterface.php | 43 ++++ .../Model/Order/Validation/RefundOrder.php | 104 ++++++++++ .../Order/Validation/RefundOrderInterface.php | 38 ++++ .../Model/Order/Validation/ShipOrder.php | 92 +++++++++ .../Order/Validation/ShipOrderInterface.php | 41 ++++ .../Magento/Sales/Model/RefundInvoice.php | 100 +++------ app/code/Magento/Sales/Model/RefundOrder.php | 78 ++----- app/code/Magento/Sales/Model/ShipOrder.php | 52 ++--- app/code/Magento/Sales/Model/Validator.php | 21 +- .../Magento/Sales/Model/ValidatorResult.php | 41 ++++ .../Sales/Model/ValidatorResultInterface.php | 29 +++ .../Sales/Model/ValidatorResultMerger.php | 46 +++++ .../Test/Unit/Model/InvoiceOrderTest.php | 133 +++++++----- .../Test/Unit/Model/RefundInvoiceTest.php | 173 +++++++++------- .../Sales/Test/Unit/Model/RefundOrderTest.php | 133 ++++++------ .../Sales/Test/Unit/Model/ShipOrderTest.php | 183 ++++++++-------- app/code/Magento/Sales/etc/di.xml | 5 + app/code/Magento/SalesInventory/LICENSE.txt | 48 +++++ .../Magento/SalesInventory/LICENSE_AFL.txt | 48 +++++ .../Model/Order/ReturnProcessor.php | 156 ++++++++++++++ .../Model/Order/ReturnValidator.php | 70 +++++++ .../Plugin/Order/ReturnToStockInvoice.php | 105 ++++++++++ .../Model/Plugin/Order/ReturnToStockOrder.php | 101 +++++++++ .../InvoiceRefundCreationArguments.php | 100 +++++++++ .../OrderRefundCreationArguments.php | 84 ++++++++ app/code/Magento/SalesInventory/README.md | 1 + .../Unit/Model/Order/ReturnProcessorTest.php | 195 ++++++++++++++++++ .../Unit/Model/Order/ReturnValidatorTest.php | 134 ++++++++++++ .../Plugin/Order/ReturnToStockInvoiceTest.php | 188 +++++++++++++++++ .../Plugin/Order/ReturnToStockOrderTest.php | 166 +++++++++++++++ .../InvoiceRefundCreationArgumentsTest.php | 158 ++++++++++++++ .../OrderRefundCreationArgumentsTest.php | 151 ++++++++++++++ app/code/Magento/SalesInventory/composer.json | 26 +++ app/code/Magento/SalesInventory/etc/di.xml | 21 ++ .../etc/extension_attributes.xml | 12 ++ .../Magento/SalesInventory/etc/module.xml | 17 ++ .../Magento/SalesInventory/registration.php | 11 + .../Adminhtml/Order/Shipment/Save.php | 11 +- .../Adminhtml/Order/Shipment/SaveTest.php | 16 +- composer.json | 1 + .../V1/ReturnItemsAfterRefundOrderTest.php | 114 ++++++++++ ...der_with_shipping_and_invoice_rollback.php | 6 + 52 files changed, 3068 insertions(+), 498 deletions(-) create mode 100644 app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/ShipOrder.php create mode 100644 app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php create mode 100644 app/code/Magento/Sales/Model/ValidatorResult.php create mode 100644 app/code/Magento/Sales/Model/ValidatorResultInterface.php create mode 100644 app/code/Magento/Sales/Model/ValidatorResultMerger.php create mode 100644 app/code/Magento/SalesInventory/LICENSE.txt create mode 100644 app/code/Magento/SalesInventory/LICENSE_AFL.txt create mode 100644 app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php create mode 100644 app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php create mode 100644 app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php create mode 100644 app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php create mode 100644 app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php create mode 100644 app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php create mode 100644 app/code/Magento/SalesInventory/README.md create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php create mode 100644 app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php create mode 100644 app/code/Magento/SalesInventory/composer.json create mode 100644 app/code/Magento/SalesInventory/etc/di.xml create mode 100644 app/code/Magento/SalesInventory/etc/extension_attributes.xml create mode 100644 app/code/Magento/SalesInventory/etc/module.xml create mode 100644 app/code/Magento/SalesInventory/registration.php create mode 100644 dev/tests/api-functional/testsuite/Magento/SalesInventory/Api/Service/V1/ReturnItemsAfterRefundOrderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php diff --git a/app/code/Magento/Sales/Model/InvoiceOrder.php b/app/code/Magento/Sales/Model/InvoiceOrder.php index e51b46082d943..c503b01a5ab21 100644 --- a/app/code/Magento/Sales/Model/InvoiceOrder.php +++ b/app/code/Magento/Sales/Model/InvoiceOrder.php @@ -12,15 +12,12 @@ use Magento\Sales\Api\InvoiceOrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order\Config as OrderConfig; -use Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface; use Magento\Sales\Model\Order\Invoice\NotifierInterface; use Magento\Sales\Model\Order\InvoiceDocumentFactory; -use Magento\Sales\Model\Order\InvoiceQuantityValidator; use Magento\Sales\Model\Order\InvoiceRepository; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; use Magento\Sales\Model\Order\PaymentAdapterInterface; -use Magento\Sales\Model\Order\Validation\CanInvoice; +use Magento\Sales\Model\Order\Validation\InvoiceOrderInterface as InvoiceOrderValidator; use Psr\Log\LoggerInterface; /** @@ -44,11 +41,6 @@ class InvoiceOrder implements InvoiceOrderInterface */ private $invoiceDocumentFactory; - /** - * @var InvoiceValidatorInterface - */ - private $invoiceValidator; - /** * @var PaymentAdapterInterface */ @@ -69,6 +61,11 @@ class InvoiceOrder implements InvoiceOrderInterface */ private $invoiceRepository; + /** + * @var InvoiceOrderValidator + */ + private $invoiceOrderValidator; + /** * @var NotifierInterface */ @@ -80,21 +77,15 @@ class InvoiceOrder implements InvoiceOrderInterface private $logger; /** - * @var OrderValidatorInterface - */ - private $orderValidator; - - /** - * OrderInvoice constructor. + * InvoiceOrder constructor. * @param ResourceConnection $resourceConnection * @param OrderRepositoryInterface $orderRepository * @param InvoiceDocumentFactory $invoiceDocumentFactory - * @param InvoiceValidatorInterface $invoiceValidator - * @param OrderValidatorInterface $orderValidator * @param PaymentAdapterInterface $paymentAdapter * @param OrderStateResolverInterface $orderStateResolver * @param OrderConfig $config * @param InvoiceRepository $invoiceRepository + * @param InvoiceOrderValidator $invoiceOrderValidator * @param NotifierInterface $notifierInterface * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -103,24 +94,22 @@ public function __construct( ResourceConnection $resourceConnection, OrderRepositoryInterface $orderRepository, InvoiceDocumentFactory $invoiceDocumentFactory, - InvoiceValidatorInterface $invoiceValidator, - OrderValidatorInterface $orderValidator, PaymentAdapterInterface $paymentAdapter, OrderStateResolverInterface $orderStateResolver, OrderConfig $config, InvoiceRepository $invoiceRepository, + InvoiceOrderValidator $invoiceOrderValidator, NotifierInterface $notifierInterface, LoggerInterface $logger ) { $this->resourceConnection = $resourceConnection; $this->orderRepository = $orderRepository; $this->invoiceDocumentFactory = $invoiceDocumentFactory; - $this->invoiceValidator = $invoiceValidator; - $this->orderValidator = $orderValidator; $this->paymentAdapter = $paymentAdapter; $this->orderStateResolver = $orderStateResolver; $this->config = $config; $this->invoiceRepository = $invoiceRepository; + $this->invoiceOrderValidator = $invoiceOrderValidator; $this->notifierInterface = $notifierInterface; $this->logger = $logger; } @@ -158,19 +147,19 @@ public function execute( ($appendComment && $notify), $arguments ); - $errorMessages = array_merge( - $this->invoiceValidator->validate( - $invoice, - [InvoiceQuantityValidator::class] - ), - $this->orderValidator->validate( - $order, - [CanInvoice::class] - ) + $errorMessages = $this->invoiceOrderValidator->validate( + $order, + $invoice, + $capture, + $items, + $notify, + $appendComment, + $comment, + $arguments ); - if (!empty($errorMessages)) { + if ($errorMessages->hasMessages()) { throw new \Magento\Sales\Exception\DocumentValidationException( - __("Invoice Document Validation Error(s):\n" . implode("\n", $errorMessages)) + __("Invoice Document Validation Error(s):\n" . implode("\n", $errorMessages->getMessages())) ); } $connection->beginTransaction(); diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php index 3889f3b985ff0..030a9a7d128de 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CreditmemoValidatorInterface.php @@ -8,6 +8,7 @@ use Magento\Sales\Api\Data\CreditmemoInterface; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; /** * Interface CreditmemoValidatorInterface @@ -17,7 +18,7 @@ interface CreditmemoValidatorInterface /** * @param CreditmemoInterface $entity * @param ValidatorInterface[] $validators - * @return string[] + * @return ValidatorResultInterface * @throws DocumentValidationException */ public function validate(CreditmemoInterface $entity, array $validators); diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php index 9f8bb84ccd16a..7a758122b8aac 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreationValidatorInterface.php @@ -7,6 +7,7 @@ use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\ValidatorResultInterface; /** * Interface ItemCreationValidatorInterface @@ -17,7 +18,7 @@ interface ItemCreationValidatorInterface * @param CreditmemoItemCreationInterface $item * @param array $validators * @param OrderInterface|null $context - * @return mixed + * @return ValidatorResultInterface */ public function validate(CreditmemoItemCreationInterface $item, array $validators, OrderInterface $context = null); } diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php index 067e3d782a88d..361e9ac4c91d4 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php @@ -7,6 +7,8 @@ /** * Class CreditmemoDocumentFactory + * + * @api */ class CreditmemoDocumentFactory { diff --git a/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php index 568019a40fce5..44d701b1426e7 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php @@ -8,6 +8,7 @@ use Magento\Sales\Api\Data\InvoiceInterface; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; /** * Interface InvoiceValidatorInterface @@ -17,7 +18,7 @@ interface InvoiceValidatorInterface /** * @param InvoiceInterface $entity * @param ValidatorInterface[] $validators - * @return string[] + * @return ValidatorResultInterface * @throws DocumentValidationException */ public function validate(InvoiceInterface $entity, array $validators); diff --git a/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php index c5a9a6c1d3296..dfc95043cbd33 100644 --- a/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php @@ -8,6 +8,7 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; /** * Interface OrderValidatorInterface @@ -17,7 +18,7 @@ interface OrderValidatorInterface /** * @param OrderInterface $entity * @param ValidatorInterface[] $validators - * @return string[] + * @return ValidatorResultInterface * @throws DocumentValidationException */ public function validate(OrderInterface $entity, array $validators); diff --git a/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php index 198a4019bf6b8..43501a5b13314 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php @@ -8,6 +8,7 @@ use Magento\Sales\Api\Data\ShipmentInterface; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; /** * Interface ShipmentValidatorInterface @@ -17,7 +18,7 @@ interface ShipmentValidatorInterface /** * @param ShipmentInterface $shipment * @param ValidatorInterface[] $validators - * @return string[] + * @return ValidatorResultInterface * @throws DocumentValidationException */ public function validate(ShipmentInterface $shipment, array $validators); diff --git a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php new file mode 100644 index 0000000000000..c0882bfd37253 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php @@ -0,0 +1,79 @@ +invoiceValidator = $invoiceValidator; + $this->orderValidator = $orderValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + OrderInterface $order, + InvoiceInterface $invoice, + $capture = false, + array $items = [], + $notify = false, + $appendComment = false, + InvoiceCommentCreationInterface $comment = null, + InvoiceCreationArgumentsInterface $arguments = null + ) { + return $this->validatorResultMerger->merge( + $this->invoiceValidator->validate( + $invoice, + [InvoiceQuantityValidator::class] + ), + $this->orderValidator->validate( + $order, + [CanInvoice::class] + ) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php new file mode 100644 index 0000000000000..92cf617846087 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php @@ -0,0 +1,42 @@ +orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->invoiceValidator = $invoiceValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + InvoiceInterface $invoice, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation[] = $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + )->getMessages(); + } + + $invoiceValidationResult = $this->invoiceValidator->validate( + $invoice, + [ + \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class + ] + ); + + return $this->validatorResultMerger->merge( + $orderValidationResult, + $creditmemoValidationResult, + $invoiceValidationResult->getMessages(), + ...array_values($itemsValidation) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php new file mode 100644 index 0000000000000..a6bd5e3dbc807 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php @@ -0,0 +1,43 @@ +orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation[] = $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + )->getMessages(); + } + + return $this->validatorResultMerger->merge( + $orderValidationResult, + $creditmemoValidationResult, + ...array_values($itemsValidation) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php new file mode 100644 index 0000000000000..dc43b470d0d1b --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php @@ -0,0 +1,38 @@ +orderValidator = $orderValidator; + $this->shipmentValidator = $shipmentValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @param OrderInterface $order + * @param ShipmentInterface $shipment + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param array $tracks + * @param array $packages + * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Model\ValidatorResultInterface + */ + public function validate( + $order, + $shipment, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + array $tracks = [], + array $packages = [], + \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanShip::class + ] + ); + $shipmentValidationResult = $this->shipmentValidator->validate( + $shipment, + [ + QuantityValidator::class, + TrackValidator::class + ] + ); + + return $this->validatorResultMerger->merge($orderValidationResult, $shipmentValidationResult); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php new file mode 100644 index 0000000000000..e3760c4dc9604 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php @@ -0,0 +1,41 @@ +orderStateResolver = $orderStateResolver; $this->orderRepository = $orderRepository; $this->invoiceRepository = $invoiceRepository; - $this->orderValidator = $orderValidator; - $this->creditmemoValidator = $creditmemoValidator; - $this->itemCreationValidator = $itemCreationValidator; + $this->validator = $validator; $this->creditmemoRepository = $creditmemoRepository; $this->paymentAdapter = $paymentAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; $this->notifier = $notifier; $this->config = $config; $this->logger = $logger; - $this->invoiceValidator = $invoiceValidator; } /** @@ -174,50 +143,27 @@ public function execute( ($appendComment && $notify), $arguments ); - $orderValidationResult = $this->orderValidator->validate( - $order, - [ - CanRefund::class - ] - ); - $invoiceValidationResult = $this->invoiceValidator->validate( + + $validationMessages = $this->validator->validate( $invoice, - [ - \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class - ] - ); - $creditmemoValidationResult = $this->creditmemoValidator->validate( + $order, $creditmemo, - [ - QuantityValidator::class, - TotalsValidator::class - ] - ); - $itemsValidation = []; - foreach ($items as $item) { - $itemsValidation = array_merge( - $itemsValidation, - $this->itemCreationValidator->validate( - $item, - [CreationQuantityValidator::class], - $order - ) - ); - } - $validationMessages = array_merge( - $orderValidationResult, - $invoiceValidationResult, - $creditmemoValidationResult, - $itemsValidation + $items, + $isOnline, + $notify, + $appendComment, + $comment, + $arguments ); - if (!empty($validationMessages )) { + if ($validationMessages->hasMessages()) { throw new \Magento\Sales\Exception\DocumentValidationException( - __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages())) ); } $connection->beginTransaction(); try { $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order->setCustomerNoteNotify($notify); $order = $this->paymentAdapter->refund($creditmemo, $order, $isOnline); $order->setState( $this->orderStateResolver->getStateForOrder($order, []) diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php index 65d81df0d1de9..abd6e25416729 100644 --- a/app/code/Magento/Sales/Model/RefundOrder.php +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -10,17 +10,11 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Api\RefundOrderInterface; use Magento\Sales\Model\Order\Config as OrderConfig; -use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; -use Magento\Sales\Model\Order\Creditmemo\ItemCreationValidatorInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; -use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; -use Magento\Sales\Model\Order\Creditmemo\Validation\QuantityValidator; -use Magento\Sales\Model\Order\Creditmemo\Validation\TotalsValidator; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; use Magento\Sales\Model\Order\PaymentAdapterInterface; -use Magento\Sales\Model\Order\Validation\CanRefund; +use Magento\Sales\Model\Order\Validation\RefundOrderInterface as RefundOrderValidator; use Psr\Log\LoggerInterface; /** @@ -44,28 +38,13 @@ class RefundOrder implements RefundOrderInterface */ private $orderRepository; - /** - * @var OrderValidatorInterface - */ - private $orderValidator; - - /** - * @var CreditmemoValidatorInterface - */ - private $creditmemoValidator; - - /** - * @var Order\Creditmemo\ItemCreationValidatorInterface - */ - private $itemCreationValidator; - /** * @var CreditmemoRepositoryInterface */ private $creditmemoRepository; /** - * @var Order\PaymentAdapterInterface + * @var PaymentAdapterInterface */ private $paymentAdapter; @@ -75,7 +54,12 @@ class RefundOrder implements RefundOrderInterface private $creditmemoDocumentFactory; /** - * @var Order\Creditmemo\NotifierInterface + * @var RefundOrderValidator + */ + private $validator; + + /** + * @var NotifierInterface */ private $notifier; @@ -91,15 +75,14 @@ class RefundOrder implements RefundOrderInterface /** * RefundOrder constructor. + * * @param ResourceConnection $resourceConnection * @param OrderStateResolverInterface $orderStateResolver * @param OrderRepositoryInterface $orderRepository - * @param OrderValidatorInterface $orderValidator - * @param CreditmemoValidatorInterface $creditmemoValidator - * @param ItemCreationValidatorInterface $itemCreationValidator * @param CreditmemoRepositoryInterface $creditmemoRepository * @param PaymentAdapterInterface $paymentAdapter * @param CreditmemoDocumentFactory $creditmemoDocumentFactory + * @param RefundOrderValidator $validator * @param NotifierInterface $notifier * @param OrderConfig $config * @param LoggerInterface $logger @@ -109,12 +92,10 @@ public function __construct( ResourceConnection $resourceConnection, OrderStateResolverInterface $orderStateResolver, OrderRepositoryInterface $orderRepository, - OrderValidatorInterface $orderValidator, - CreditmemoValidatorInterface $creditmemoValidator, - ItemCreationValidatorInterface $itemCreationValidator, CreditmemoRepositoryInterface $creditmemoRepository, PaymentAdapterInterface $paymentAdapter, CreditmemoDocumentFactory $creditmemoDocumentFactory, + RefundOrderValidator $validator, NotifierInterface $notifier, OrderConfig $config, LoggerInterface $logger @@ -122,12 +103,10 @@ public function __construct( $this->resourceConnection = $resourceConnection; $this->orderStateResolver = $orderStateResolver; $this->orderRepository = $orderRepository; - $this->orderValidator = $orderValidator; - $this->creditmemoValidator = $creditmemoValidator; - $this->itemCreationValidator = $itemCreationValidator; $this->creditmemoRepository = $creditmemoRepository; $this->paymentAdapter = $paymentAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; + $this->validator = $validator; $this->notifier = $notifier; $this->config = $config; $this->logger = $logger; @@ -153,39 +132,24 @@ public function execute( ($appendComment && $notify), $arguments ); - $orderValidationResult = $this->orderValidator->validate( + $validationMessages = $this->validator->validate( $order, - [ - CanRefund::class - ] - ); - $creditmemoValidationResult = $this->creditmemoValidator->validate( $creditmemo, - [ - QuantityValidator::class, - TotalsValidator::class - ] + $items, + $notify, + $appendComment, + $comment, + $arguments ); - $itemsValidation = []; - foreach ($items as $item) { - $itemsValidation = array_merge( - $itemsValidation, - $this->itemCreationValidator->validate( - $item, - [CreationQuantityValidator::class], - $order - ) - ); - } - $validationMessages = array_merge($orderValidationResult, $creditmemoValidationResult, $itemsValidation); - if (!empty($validationMessages)) { + if ($validationMessages->hasMessages()) { throw new \Magento\Sales\Exception\DocumentValidationException( - __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages)) + __("Creditmemo Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages())) ); } $connection->beginTransaction(); try { $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); + $order->setCustomerNoteNotify($notify); $order = $this->paymentAdapter->refund($creditmemo, $order); $order->setState( $this->orderStateResolver->getStateForOrder($order, []) diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php index d051144cf73ca..034442a19c1f7 100644 --- a/app/code/Magento/Sales/Model/ShipOrder.php +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -11,14 +11,10 @@ use Magento\Sales\Api\ShipOrderInterface; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Shipment\NotifierInterface; -use Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface; -use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; -use Magento\Sales\Model\Order\Shipment\Validation\TrackValidator; -use Magento\Sales\Model\Order\Validation\CanShip; use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface; +use Magento\Sales\Model\Order\Validation\ShipOrderInterface as ShipOrderValidator; use Psr\Log\LoggerInterface; /** @@ -42,11 +38,6 @@ class ShipOrder implements ShipOrderInterface */ private $shipmentDocumentFactory; - /** - * @var ShipmentValidatorInterface - */ - private $shipmentValidator; - /** * @var OrderStateResolverInterface */ @@ -62,6 +53,11 @@ class ShipOrder implements ShipOrderInterface */ private $shipmentRepository; + /** + * @var ShipOrderValidator + */ + private $shipOrderValidator; + /** * @var NotifierInterface */ @@ -72,11 +68,6 @@ class ShipOrder implements ShipOrderInterface */ private $logger; - /** - * @var OrderValidatorInterface - */ - private $orderValidator; - /** * @var OrderRegistrarInterface */ @@ -86,11 +77,10 @@ class ShipOrder implements ShipOrderInterface * @param ResourceConnection $resourceConnection * @param OrderRepositoryInterface $orderRepository * @param ShipmentDocumentFactory $shipmentDocumentFactory - * @param ShipmentValidatorInterface $shipmentValidator - * @param OrderValidatorInterface $orderValidator * @param OrderStateResolverInterface $orderStateResolver * @param OrderConfig $config * @param ShipmentRepositoryInterface $shipmentRepository + * @param ShipOrderValidator $shipOrderValidator * @param NotifierInterface $notifierInterface * @param OrderRegistrarInterface $orderRegistrar * @param LoggerInterface $logger @@ -100,11 +90,10 @@ public function __construct( ResourceConnection $resourceConnection, OrderRepositoryInterface $orderRepository, ShipmentDocumentFactory $shipmentDocumentFactory, - ShipmentValidatorInterface $shipmentValidator, - OrderValidatorInterface $orderValidator, OrderStateResolverInterface $orderStateResolver, OrderConfig $config, ShipmentRepositoryInterface $shipmentRepository, + ShipOrderValidator $shipOrderValidator, NotifierInterface $notifierInterface, OrderRegistrarInterface $orderRegistrar, LoggerInterface $logger @@ -112,11 +101,10 @@ public function __construct( $this->resourceConnection = $resourceConnection; $this->orderRepository = $orderRepository; $this->shipmentDocumentFactory = $shipmentDocumentFactory; - $this->shipmentValidator = $shipmentValidator; - $this->orderValidator = $orderValidator; $this->orderStateResolver = $orderStateResolver; $this->config = $config; $this->shipmentRepository = $shipmentRepository; + $this->shipOrderValidator = $shipOrderValidator; $this->notifierInterface = $notifierInterface; $this->logger = $logger; $this->orderRegistrar = $orderRegistrar; @@ -159,23 +147,19 @@ public function execute( $packages, $arguments ); - $orderValidationResult = $this->orderValidator->validate( + $validationMessages = $this->shipOrderValidator->validate( $order, - [ - CanShip::class - ] - ); - $shipmentValidationResult = $this->shipmentValidator->validate( $shipment, - [ - QuantityValidator::class, - TrackValidator::class - ] + $items, + $notify, + $appendComment, + $comment, + $tracks, + $packages ); - $validationMessages = array_merge($orderValidationResult, $shipmentValidationResult); - if (!empty($validationMessages)) { + if ($validationMessages->hasMessages()) { throw new \Magento\Sales\Exception\DocumentValidationException( - __("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages)) + __("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages())) ); } $connection->beginTransaction(); diff --git a/app/code/Magento/Sales/Model/Validator.php b/app/code/Magento/Sales/Model/Validator.php index c0914faf2ceea..281d517bdc2e5 100644 --- a/app/code/Magento/Sales/Model/Validator.php +++ b/app/code/Magento/Sales/Model/Validator.php @@ -20,21 +20,30 @@ class Validator */ private $objectManager; + /** + * @var ValidatorResultInterfaceFactory + */ + private $validatorResultFactory; + /** * Validator constructor. * * @param ObjectManagerInterface $objectManager + * @param ValidatorResultInterfaceFactory $validatorResult */ - public function __construct(ObjectManagerInterface $objectManager) - { + public function __construct( + ObjectManagerInterface $objectManager, + ValidatorResultInterfaceFactory $validatorResult + ) { $this->objectManager = $objectManager; + $this->validatorResultFactory = $validatorResult; } /** * @param object $entity * @param ValidatorInterface[] $validators * @param object|null $context - * @return \string[] + * @return ValidatorResultInterface * @throws LocalizedException */ public function validate($entity, array $validators, $context = null) @@ -56,7 +65,11 @@ public function validate($entity, array $validators, $context = null) } $messages = array_merge($messages, $validator->validate($entity)); } + $validationResult = $this->validatorResultFactory->create(); + foreach ($messages as $message) { + $validationResult->addMessage($message); + } - return $messages; + return $validationResult; } } diff --git a/app/code/Magento/Sales/Model/ValidatorResult.php b/app/code/Magento/Sales/Model/ValidatorResult.php new file mode 100644 index 0000000000000..32137ebdafcf3 --- /dev/null +++ b/app/code/Magento/Sales/Model/ValidatorResult.php @@ -0,0 +1,41 @@ +messages[] = (string)$message; + } + + /** + * @return bool + */ + public function hasMessages() + { + return count($this->messages) > 0; + } + + /** + * @return string[] + */ + public function getMessages() + { + return $this->messages; + } +} diff --git a/app/code/Magento/Sales/Model/ValidatorResultInterface.php b/app/code/Magento/Sales/Model/ValidatorResultInterface.php new file mode 100644 index 0000000000000..2de57cb9f1485 --- /dev/null +++ b/app/code/Magento/Sales/Model/ValidatorResultInterface.php @@ -0,0 +1,29 @@ +validatorResultInterfaceFactory = $validatorResultInterfaceFactory; + } + + /** + * Merge two validator results and additional messages + * + * @param ValidatorResultInterface $first + * @param ValidatorResultInterface $second + * @return ValidatorResultInterface + */ + public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second) + { + $messages = array_merge($first->getMessages(), $second->getMessages(), ...array_slice(func_get_args(), 2)); + + $result = $this->validatorResultInterfaceFactory->create(); + foreach ($messages as $message) { + $result->addMessage($message); + } + + return $result; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php index 6dfa929acb629..1169e230e7542 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php @@ -14,19 +14,19 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Config as OrderConfig; -use Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface; use Magento\Sales\Model\Order\Invoice\NotifierInterface; use Magento\Sales\Model\Order\InvoiceDocumentFactory; use Magento\Sales\Model\Order\InvoiceRepository; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\Validation\InvoiceOrderInterface; use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\ValidatorResultInterface; use Magento\Sales\Model\InvoiceOrder; use Psr\Log\LoggerInterface; /** * Class InvoiceOrderTest - * + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -48,14 +48,9 @@ class InvoiceOrderTest extends \PHPUnit_Framework_TestCase private $invoiceDocumentFactoryMock; /** - * @var InvoiceValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var InvoiceOrderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $invoiceValidatorMock; - - /** - * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $orderValidatorMock; + private $invoiceOrderValidatorMock; /** * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject @@ -117,6 +112,11 @@ class InvoiceOrderTest extends \PHPUnit_Framework_TestCase */ private $loggerMock; + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $errorMessagesMock; + protected function setUp() { $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) @@ -131,14 +131,6 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -183,22 +175,37 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->invoiceOrderValidatorMock = $this->getMockBuilder(InvoiceOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->errorMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['hasMessages', 'getMessages', 'addMessage']) + ->getMock(); + $this->invoiceOrder = new InvoiceOrder( $this->resourceConnectionMock, $this->orderRepositoryMock, $this->invoiceDocumentFactoryMock, - $this->invoiceValidatorMock, - $this->orderValidatorMock, $this->paymentAdapterMock, $this->orderStateResolverMock, $this->configMock, $this->invoiceRepositoryMock, + $this->invoiceOrderValidatorMock, $this->notifierInterfaceMock, $this->loggerMock ); } /** + * @param int $orderId + * @param bool $capture + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @throws \Magento\Sales\Exception\CouldNotInvoiceException + * @throws \Magento\Sales\Exception\DocumentValidationException * @dataProvider dataProvider */ public function testOrderInvoice($orderId, $capture, $items, $notify, $appendComment) @@ -207,11 +214,9 @@ public function testOrderInvoice($orderId, $capture, $items, $notify, $appendCom ->method('getConnection') ->with('sales') ->willReturn($this->adapterInterface); - $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); - $this->invoiceDocumentFactoryMock->expects($this->once()) ->method('create') ->with( @@ -221,66 +226,62 @@ public function testOrderInvoice($orderId, $capture, $items, $notify, $appendCom ($appendComment && $notify), $this->invoiceCreationArgumentsMock )->willReturn($this->invoiceMock); - - $this->invoiceValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->invoiceMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) + $this->invoiceOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - + ->with( + $this->orderMock, + $this->invoiceMock, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ) + ->willReturn($this->errorMessagesMock); + $hasMessages = false; + $this->errorMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $this->paymentAdapterMock->expects($this->once()) ->method('pay') ->with($this->orderMock, $this->invoiceMock, $capture) ->willReturn($this->orderMock); - $this->orderStateResolverMock->expects($this->once()) ->method('getStateForOrder') ->with($this->orderMock, [OrderStateResolverInterface::IN_PROGRESS]) ->willReturn(Order::STATE_PROCESSING); - $this->orderMock->expects($this->once()) ->method('setState') ->with(Order::STATE_PROCESSING) ->willReturnSelf(); - $this->orderMock->expects($this->once()) ->method('getState') ->willReturn(Order::STATE_PROCESSING); - $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') ->with(Order::STATE_PROCESSING) ->willReturn('Processing'); - $this->orderMock->expects($this->once()) ->method('setStatus') ->with('Processing') ->willReturnSelf(); - $this->invoiceMock->expects($this->once()) ->method('setState') ->with(\Magento\Sales\Model\Order\Invoice::STATE_PAID) ->willReturnSelf(); - $this->invoiceRepositoryMock->expects($this->once()) ->method('save') ->with($this->invoiceMock) ->willReturn($this->invoiceMock); - $this->orderRepositoryMock->expects($this->once()) ->method('save') ->with($this->orderMock) ->willReturn($this->orderMock); - if ($notify) { $this->notifierInterfaceMock->expects($this->once()) ->method('notify') ->with($this->orderMock, $this->invoiceMock, $this->invoiceCommentCreationMock); } - $this->invoiceMock->expects($this->once()) ->method('getEntityId') ->willReturn(2); @@ -325,14 +326,25 @@ public function testDocumentValidationException() $this->invoiceCreationArgumentsMock )->willReturn($this->invoiceMock); - $this->invoiceValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->invoiceMock) - ->willReturn($errorMessages); - $this->orderValidatorMock->expects($this->once()) + $this->invoiceOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->orderMock) - ->willReturn([]); + ->with( + $this->orderMock, + $this->invoiceMock, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ) + ->willReturn($this->errorMessagesMock); + $hasMessages = true; + + $this->errorMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + $this->errorMessagesMock->expects($this->once()) + ->method('getMessages')->willReturn($errorMessages); $this->invoiceOrder->execute( $orderId, @@ -374,16 +386,25 @@ public function testCouldNotInvoiceException() $this->invoiceCreationArgumentsMock )->willReturn($this->invoiceMock); - $this->invoiceValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->invoiceMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) + $this->invoiceOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $e = new \Exception(); + ->with( + $this->orderMock, + $this->invoiceMock, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ) + ->willReturn($this->errorMessagesMock); + $hasMessages = false; + $this->errorMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + + $e = new \Exception(); $this->paymentAdapterMock->expects($this->once()) ->method('pay') ->with($this->orderMock, $this->invoiceMock, $capture) diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php index 31c8858a249a9..d249f039a140b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -17,13 +17,13 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Config as OrderConfig; -use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; +use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; -use Magento\Sales\Model\Order\Invoice\InvoiceValidatorInterface; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; use Magento\Sales\Model\Order\PaymentAdapterInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; +use Magento\Sales\Model\Order\Validation\RefundInvoiceInterface; +use Magento\Sales\Model\ValidatorResultInterface; use Magento\Sales\Model\RefundInvoice; use Psr\Log\LoggerInterface; @@ -54,21 +54,6 @@ class RefundInvoiceTest extends \PHPUnit_Framework_TestCase */ private $creditmemoDocumentFactoryMock; - /** - * @var CreditmemoValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $creditmemoValidatorMock; - - /** - * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $orderValidatorMock; - - /** - * @var InvoiceValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $invoiceValidatorMock; - /** * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -129,6 +114,21 @@ class RefundInvoiceTest extends \PHPUnit_Framework_TestCase */ private $adapterInterface; + /** + * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $creditmemoItemCreationMock; + + /** + * @var RefundInvoiceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $refundInvoiceValidatorMock; + + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationMessagesMock; + /** * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -148,24 +148,15 @@ protected function setUp() $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); - - $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + $this->refundInvoiceValidatorMock = $this->getMockBuilder(RefundInvoiceInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->configMock = $this->getMockBuilder(OrderConfig::class) ->disableOriginalConstructor() ->getMock(); @@ -206,14 +197,20 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['hasMessages', 'getMessages', 'addMessage']) + ->getMock(); + $this->refundInvoice = new RefundInvoice( $this->resourceConnectionMock, $this->orderStateResolverMock, $this->orderRepositoryMock, $this->invoiceRepositoryMock, - $this->orderValidatorMock, - $this->invoiceValidatorMock, - $this->creditmemoValidatorMock, + $this->refundInvoiceValidatorMock, $this->creditmemoRepositoryMock, $this->paymentAdapterMock, $this->creditmemoDocumentFactoryMock, @@ -224,22 +221,27 @@ protected function setUp() } /** + * @param int $invoiceId + * @param bool $isOnline + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @throws \Magento\Sales\Exception\CouldNotRefundException + * @throws \Magento\Sales\Exception\DocumentValidationException * @dataProvider dataProvider */ - public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) + public function testOrderCreditmemo($invoiceId, $isOnline, $items, $notify, $appendComment) { $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with('sales') ->willReturn($this->adapterInterface); - $this->invoiceRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->invoiceMock); $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); - $this->creditmemoDocumentFactoryMock->expects($this->once()) ->method('createFromInvoice') ->with( @@ -249,19 +251,23 @@ public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) ($appendComment && $notify), $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - - $this->creditmemoValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->creditmemoMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $this->invoiceValidatorMock->expects($this->once()) + $this->refundInvoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->invoiceMock) - ->willReturn([]); + ->with( + $this->invoiceMock, + $this->orderMock, + $this->creditmemoMock, + $items, + $isOnline, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $this->paymentAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) @@ -289,7 +295,6 @@ public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) ->method('setState') ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) ->willReturnSelf(); - $this->creditmemoRepositoryMock->expects($this->once()) ->method('save') ->with($this->creditmemoMock) @@ -312,7 +317,7 @@ public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) $this->refundInvoice->execute( $invoiceId, $items, - false, + true, $notify, $appendComment, $this->creditmemoCommentCreationMock, @@ -327,9 +332,10 @@ public function testOrderCreditmemo($invoiceId, $items, $notify, $appendComment) public function testDocumentValidationException() { $invoiceId = 1; - $items = [1 => 2]; + $items = [1 => $this->creditmemoItemCreationMock]; $notify = true; $appendComment = true; + $isOnline = false; $errorMessages = ['error1', 'error2']; $this->invoiceRepositoryMock->expects($this->once()) @@ -349,18 +355,25 @@ public function testDocumentValidationException() $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->creditmemoValidatorMock->expects($this->once()) + $this->refundInvoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->creditmemoMock) - ->willReturn($errorMessages); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $this->invoiceValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->invoiceMock) - ->willReturn([]); + ->with( + $this->invoiceMock, + $this->orderMock, + $this->creditmemoMock, + $items, + $isOnline, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = true; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + $this->validationMessagesMock->expects($this->once()) + ->method('getMessages')->willReturn($errorMessages); $this->assertEquals( $errorMessages, @@ -382,9 +395,10 @@ public function testDocumentValidationException() public function testCouldNotCreditmemoException() { $invoiceId = 1; - $items = [1 => 2]; + $items = [1 => $this->creditmemoItemCreationMock]; $notify = true; $appendComment = true; + $isOnline = false; $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with('sales') @@ -407,18 +421,23 @@ public function testCouldNotCreditmemoException() $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->creditmemoValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->creditmemoMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) + $this->refundInvoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $this->invoiceValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->invoiceMock) - ->willReturn([]); + ->with( + $this->invoiceMock, + $this->orderMock, + $this->creditmemoMock, + $items, + $isOnline, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $e = new \Exception(); $this->paymentAdapterMock->expects($this->once()) @@ -446,9 +465,13 @@ public function testCouldNotCreditmemoException() public function dataProvider() { + $creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + return [ - 'TestWithNotifyTrue' => [1, [1 => 2], true, true], - 'TestWithNotifyFalse' => [1, [1 => 2], false, true], + 'TestWithNotifyTrue' => [1, true, [1 => $creditmemoItemCreationMock], true, true], + 'TestWithNotifyFalse' => [1, true, [1 => $creditmemoItemCreationMock], false, true], ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index 7d684695664ba..e06848452e712 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -15,14 +15,13 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Config as OrderConfig; -use Magento\Sales\Model\Order\Creditmemo\CreditmemoValidatorInterface; -use Magento\Sales\Model\Order\Creditmemo\Item\Validation\CreationQuantityValidator; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; +use Magento\Sales\Model\Order\Validation\RefundOrderInterface; use Magento\Sales\Model\Order\PaymentAdapterInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; use Magento\Sales\Model\RefundOrder; +use Magento\Sales\Model\ValidatorResultInterface; use Psr\Log\LoggerInterface; use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; @@ -48,16 +47,6 @@ class RefundOrderTest extends \PHPUnit_Framework_TestCase */ private $creditmemoDocumentFactoryMock; - /** - * @var CreditmemoValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $creditmemoValidatorMock; - - /** - * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $orderValidatorMock; - /** * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -119,15 +108,20 @@ class RefundOrderTest extends \PHPUnit_Framework_TestCase private $loggerMock; /** - * @var Order\Creditmemo\ItemCreationValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RefundOrderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $itemCreationValidatorMock; + private $refundOrderValidatorMock; /** * @var CreditmemoItemCreationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $creditmemoItemCreationMock; + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationMessagesMock; + protected function setUp() { $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) @@ -139,10 +133,7 @@ protected function setUp() $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoValidatorMock = $this->getMockBuilder(CreditmemoValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + $this->refundOrderValidatorMock = $this->getMockBuilder(RefundOrderInterface::class) ->disableOriginalConstructor() ->getMock(); $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) @@ -178,23 +169,22 @@ protected function setUp() $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->itemCreationValidatorMock = $this->getMockBuilder(Order\Creditmemo\ItemCreationValidatorInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['hasMessages', 'getMessages', 'addMessage']) + ->getMock(); $this->refundOrder = new RefundOrder( $this->resourceConnectionMock, $this->orderStateResolverMock, $this->orderRepositoryMock, - $this->orderValidatorMock, - $this->creditmemoValidatorMock, - $this->itemCreationValidatorMock, $this->creditmemoRepositoryMock, $this->paymentAdapterMock, $this->creditmemoDocumentFactoryMock, + $this->refundOrderValidatorMock, $this->notifierMock, $this->configMock, $this->loggerMock @@ -202,6 +192,11 @@ protected function setUp() } /** + * @param int $orderId + * @param bool $notify + * @param bool $appendComment + * @throws \Magento\Sales\Exception\CouldNotRefundException + * @throws \Magento\Sales\Exception\DocumentValidationException * @dataProvider dataProvider */ public function testOrderCreditmemo($orderId, $notify, $appendComment) @@ -223,21 +218,21 @@ public function testOrderCreditmemo($orderId, $notify, $appendComment) ($appendComment && $notify), $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->creditmemoValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->creditmemoMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $this->itemCreationValidatorMock->expects($this->once()) + $this->refundOrderValidatorMock->expects($this->once()) ->method('validate') ->with( - reset($items), - [CreationQuantityValidator::class], - $this->orderMock - )->willReturn([]); + $this->orderMock, + $this->creditmemoMock, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $this->paymentAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) @@ -246,47 +241,38 @@ public function testOrderCreditmemo($orderId, $notify, $appendComment) ->method('getStateForOrder') ->with($this->orderMock, []) ->willReturn(Order::STATE_CLOSED); - $this->orderMock->expects($this->once()) ->method('setState') ->with(Order::STATE_CLOSED) ->willReturnSelf(); - $this->orderMock->expects($this->once()) ->method('getState') ->willReturn(Order::STATE_CLOSED); - $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') ->with(Order::STATE_CLOSED) ->willReturn('Closed'); - $this->orderMock->expects($this->once()) ->method('setStatus') ->with('Closed') ->willReturnSelf(); - $this->creditmemoMock->expects($this->once()) ->method('setState') ->with(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED) ->willReturnSelf(); - $this->creditmemoRepositoryMock->expects($this->once()) ->method('save') ->with($this->creditmemoMock) ->willReturn($this->creditmemoMock); - $this->orderRepositoryMock->expects($this->once()) ->method('save') ->with($this->orderMock) ->willReturn($this->orderMock); - if ($notify) { $this->notifierMock->expects($this->once()) ->method('notify') ->with($this->orderMock, $this->creditmemoMock, $this->creditmemoCommentCreationMock); } - $this->creditmemoMock->expects($this->once()) ->method('getEntityId') ->willReturn(2); @@ -329,18 +315,24 @@ public function testDocumentValidationException() $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->creditmemoValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->creditmemoMock) - ->willReturn($errorMessages); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $this->itemCreationValidatorMock->expects($this->once()) + + $this->refundOrderValidatorMock->expects($this->once()) ->method('validate') - ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) - ->willReturn([]); + ->with( + $this->orderMock, + $this->creditmemoMock, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = true; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + $this->validationMessagesMock->expects($this->once()) + ->method('getMessages')->willReturn($errorMessages); $this->assertEquals( $errorMessages, @@ -380,18 +372,21 @@ public function testCouldNotCreditmemoException() ($appendComment && $notify), $this->creditmemoCreationArgumentsMock )->willReturn($this->creditmemoMock); - $this->itemCreationValidatorMock->expects($this->once()) - ->method('validate') - ->with(reset($items), [CreationQuantityValidator::class], $this->orderMock) - ->willReturn([]); - $this->creditmemoValidatorMock->expects($this->once()) + $this->refundOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->creditmemoMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); + ->with( + $this->orderMock, + $this->creditmemoMock, + $items, + $notify, + $appendComment, + $this->creditmemoCommentCreationMock, + $this->creditmemoCreationArgumentsMock + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $e = new \Exception(); $this->paymentAdapterMock->expects($this->once()) ->method('refund') diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php index b719babf209f0..1daf7a64263b8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -18,11 +18,11 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\OrderValidatorInterface; use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Shipment\NotifierInterface; use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface; -use Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface; +use Magento\Sales\Model\Order\Validation\ShipOrderInterface; +use Magento\Sales\Model\ValidatorResultInterface; use Magento\Sales\Model\ShipOrder; use Psr\Log\LoggerInterface; @@ -49,14 +49,9 @@ class ShipOrderTest extends \PHPUnit_Framework_TestCase private $shipmentDocumentFactoryMock; /** - * @var ShipmentValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ShipOrderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $shipmentValidatorMock; - - /** - * @var OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $orderValidatorMock; + private $shipOrderValidatorMock; /** * @var OrderRegistrarInterface|\PHPUnit_Framework_MockObject_MockObject @@ -109,20 +104,25 @@ class ShipOrderTest extends \PHPUnit_Framework_TestCase private $shipmentMock; /** - * @var AdapterInterface + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ private $adapterMock; /** - * @var ShipmentTrackCreationInterface + * @var ShipmentTrackCreationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $trackMock; /** - * @var ShipmentPackageInterface + * @var ShipmentPackageInterface|\PHPUnit_Framework_MockObject_MockObject */ private $packageMock; + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationMessagesMock; + /** * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -133,75 +133,58 @@ protected function setUp() $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->shipmentDocumentFactoryMock = $this->getMockBuilder(ShipmentDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - - $this->shipmentValidatorMock = $this->getMockBuilder(ShipmentValidatorInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->orderRegistrarMock = $this->getMockBuilder(OrderRegistrarInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->configMock = $this->getMockBuilder(OrderConfig::class) ->disableOriginalConstructor() ->getMock(); - $this->shipmentRepositoryMock = $this->getMockBuilder(ShipmentRepositoryInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->notifierInterfaceMock = $this->getMockBuilder(NotifierInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->shipmentCommentCreationMock = $this->getMockBuilder(ShipmentCommentCreationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->shipmentCreationArgumentsMock = $this->getMockBuilder(ShipmentCreationArgumentsInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->orderMock = $this->getMockBuilder(OrderInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->shipmentMock = $this->getMockBuilder(ShipmentInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->packageMock = $this->getMockBuilder(ShipmentPackageInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->trackMock = $this->getMockBuilder(ShipmentTrackCreationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->adapterMock = $this->getMockBuilder(AdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->shipOrderValidatorMock = $this->getMockBuilder(ShipOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['hasMessages', 'getMessages', 'addMessage']) + ->getMock(); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $helper->getObject( @@ -209,20 +192,25 @@ protected function setUp() [ 'resourceConnection' => $this->resourceConnectionMock, 'orderRepository' => $this->orderRepositoryMock, - 'shipmentRepository' => $this->shipmentRepositoryMock, 'shipmentDocumentFactory' => $this->shipmentDocumentFactoryMock, - 'shipmentValidator' => $this->shipmentValidatorMock, - 'orderValidator' => $this->orderValidatorMock, 'orderStateResolver' => $this->orderStateResolverMock, - 'orderRegistrar' => $this->orderRegistrarMock, - 'notifierInterface' => $this->notifierInterfaceMock, 'config' => $this->configMock, - 'logger' => $this->loggerMock + 'shipmentRepository' => $this->shipmentRepositoryMock, + 'shipOrderValidator' => $this->shipOrderValidatorMock, + 'notifierInterface' => $this->notifierInterfaceMock, + 'logger' => $this->loggerMock, + 'orderRegistrar' => $this->orderRegistrarMock ] ); } /** + * @param int $orderId + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @throws \Magento\Sales\Exception\CouldNotShipException + * @throws \Magento\Sales\Exception\DocumentValidationException * @dataProvider dataProvider */ public function testExecute($orderId, $items, $notify, $appendComment) @@ -231,11 +219,9 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('getConnection') ->with('sales') ->willReturn($this->adapterMock); - $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); - $this->shipmentDocumentFactoryMock->expects($this->once()) ->method('create') ->with( @@ -247,65 +233,61 @@ public function testExecute($orderId, $items, $notify, $appendComment) [$this->packageMock], $this->shipmentCreationArgumentsMock )->willReturn($this->shipmentMock); - - $this->shipmentValidatorMock->expects($this->once()) + $this->shipOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->shipmentMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - + ->with( + $this->orderMock, + $this->shipmentMock, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock] + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); $this->orderRegistrarMock->expects($this->once()) ->method('register') ->with($this->orderMock, $this->shipmentMock) ->willReturn($this->orderMock); - $this->orderStateResolverMock->expects($this->once()) ->method('getStateForOrder') ->with($this->orderMock, [OrderStateResolverInterface::IN_PROGRESS]) ->willReturn(Order::STATE_PROCESSING); - $this->orderMock->expects($this->once()) ->method('setState') ->with(Order::STATE_PROCESSING) ->willReturnSelf(); - $this->orderMock->expects($this->once()) ->method('getState') ->willReturn(Order::STATE_PROCESSING); - $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') ->with(Order::STATE_PROCESSING) ->willReturn('Processing'); - $this->orderMock->expects($this->once()) ->method('setStatus') ->with('Processing') ->willReturnSelf(); - $this->shipmentRepositoryMock->expects($this->once()) ->method('save') ->with($this->shipmentMock) ->willReturn($this->shipmentMock); - $this->orderRepositoryMock->expects($this->once()) ->method('save') ->with($this->orderMock) ->willReturn($this->orderMock); - if ($notify) { $this->notifierInterfaceMock->expects($this->once()) ->method('notify') ->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock); } - $this->shipmentMock->expects($this->once()) ->method('getEntityId') ->willReturn(2); - $this->assertEquals( 2, $this->model->execute( @@ -348,14 +330,24 @@ public function testDocumentValidationException() $this->shipmentCreationArgumentsMock )->willReturn($this->shipmentMock); - $this->shipmentValidatorMock->expects($this->once()) + $this->shipOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->shipmentMock) - ->willReturn($errorMessages); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); + ->with( + $this->orderMock, + $this->shipmentMock, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock] + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = true; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + $this->validationMessagesMock->expects($this->once()) + ->method('getMessages')->willReturn($errorMessages); $this->model->execute( $orderId, @@ -372,9 +364,12 @@ public function testDocumentValidationException() /** * @expectedException \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface */ - public function testCouldNotInvoiceException() + public function testCouldNotShipException() { $orderId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with('sales') @@ -387,33 +382,53 @@ public function testCouldNotInvoiceException() $this->shipmentDocumentFactoryMock->expects($this->once()) ->method('create') ->with( - $this->orderMock + $this->orderMock, + $items, + [$this->trackMock], + $this->shipmentCommentCreationMock, + ($appendComment && $notify), + [$this->packageMock], + $this->shipmentCreationArgumentsMock )->willReturn($this->shipmentMock); - - $this->shipmentValidatorMock->expects($this->once()) + $this->shipOrderValidatorMock->expects($this->once()) ->method('validate') - ->with($this->shipmentMock) - ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('validate') - ->with($this->orderMock) - ->willReturn([]); - $e = new \Exception(); + ->with( + $this->orderMock, + $this->shipmentMock, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock] + ) + ->willReturn($this->validationMessagesMock); + $hasMessages = false; + $this->validationMessagesMock->expects($this->once()) + ->method('hasMessages')->willReturn($hasMessages); + $exception = new \Exception(); $this->orderRegistrarMock->expects($this->once()) ->method('register') ->with($this->orderMock, $this->shipmentMock) - ->willThrowException($e); + ->willThrowException($exception); $this->loggerMock->expects($this->once()) ->method('critical') - ->with($e); + ->with($exception); $this->adapterMock->expects($this->once()) ->method('rollBack'); $this->model->execute( - $orderId + $orderId, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock], + $this->shipmentCreationArgumentsMock ); } diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 71c9bc61fa6ad..0e1ef5a1109a3 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -104,6 +104,11 @@ + + + + + diff --git a/app/code/Magento/SalesInventory/LICENSE.txt b/app/code/Magento/SalesInventory/LICENSE.txt new file mode 100644 index 0000000000000..b3c18b0d82db7 --- /dev/null +++ b/app/code/Magento/SalesInventory/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/SalesInventory/LICENSE_AFL.txt b/app/code/Magento/SalesInventory/LICENSE_AFL.txt new file mode 100644 index 0000000000000..1250db2538e18 --- /dev/null +++ b/app/code/Magento/SalesInventory/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php new file mode 100644 index 0000000000000..06ac2e2de7636 --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php @@ -0,0 +1,156 @@ +stockManagement = $stockManagement; + $this->stockIndexerProcessor = $stockIndexer; + $this->priceIndexer = $priceIndexer; + $this->creditmemoRepository = $creditmemoRepository; + $this->storeManager = $storeManager; + $this->orderRepository = $orderRepository; + $this->orderItemRepository = $orderItemRepository; + } + + /** + * @param CreditmemoInterface $creditmemo + * @param OrderInterface $order + * @param array $returnToStockItems + * @return void + */ + public function execute( + CreditmemoInterface $creditmemo, + OrderInterface $order, + array $returnToStockItems = [] + ) { + $itemsToUpdate = []; + foreach ($creditmemo->getItems() as $item) { + $qty = $item->getQty(); + $productId = $item->getProductId(); + $orderItem = $this->orderItemRepository->get($item->getOrderItemId()); + $parentItemId = $orderItem->getParentItemId(); + if ($this->canReturnItem($item, $qty, $parentItemId, $returnToStockItems)) { + $parentItem = $parentItemId ? $this->getItemByOrderId($creditmemo, $parentItemId) : false; + $qty = $parentItem ? $parentItem->getQty() * $qty : $qty; + if (isset($itemsToUpdate[$productId])) { + $itemsToUpdate[$productId] += $qty; + } else { + $itemsToUpdate[$productId] = $qty; + } + } + } + + if (!empty($itemsToUpdate)) { + $store = $this->storeManager->getStore($order->getStoreId()); + foreach ($itemsToUpdate as $productId => $qty) { + $this->stockManagement->backItemQty( + $productId, + $qty, + $store->getWebsiteId() + ); + } + + $updatedItemIds = array_keys($itemsToUpdate); + $this->stockIndexerProcessor->reindexList($updatedItemIds); + $this->priceIndexer->reindexList($updatedItemIds); + } + } + + /** + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param int $parentItemId + * @return bool|CreditmemoItemInterface + */ + private function getItemByOrderId(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, $parentItemId) + { + foreach ($creditmemo->getItems() as $item) { + if ($item->getOrderItemId() == $parentItemId) { + return $item; + } + } + return false; + } + + /** + * @param \Magento\Sales\Api\Data\CreditmemoItemInterface $item + * @param int $qty + * @param int[] $returnToStockItems + * @param int $parentItemId + * @return bool + */ + private function canReturnItem( + \Magento\Sales\Api\Data\CreditmemoItemInterface $item, + $qty, + $parentItemId = null, + array $returnToStockItems = [] + ) { + return (in_array($item->getOrderItemId(), $returnToStockItems) || in_array($parentItemId, $returnToStockItems)) + && $qty; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php b/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php new file mode 100644 index 0000000000000..85ee15e2ce6ac --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php @@ -0,0 +1,70 @@ +orderItemRepository = $orderItemRepository; + } + + /** + * @param int[] $returnToStockItems + * @param CreditmemoInterface $creditmemo + * @return \Magento\Framework\Phrase|null + */ + public function validate($returnToStockItems, CreditmemoInterface $creditmemo) + { + $creditmemoItems = $creditmemo->getItems(); + + /** @var int $item */ + foreach ($returnToStockItems as $item) { + try { + $orderItem = $this->orderItemRepository->get($item); + if (!$this->isOrderItemPartOfCreditmemo($creditmemoItems, $orderItem)) { + return __('The "%1" product is not part of the current creditmemo.', $orderItem->getSku()); + } + } catch (NoSuchEntityException $e) { + return __('The return to stock argument contains product item that is not part of the original order.'); + } + } + return null; + } + + /** + * @param CreditmemoItemInterface[] $creditmemoItems + * @param OrderItemInterface $orderItem + * @return bool + */ + private function isOrderItemPartOfCreditmemo(array $creditmemoItems, OrderItemInterface $orderItem) + { + foreach ($creditmemoItems as $creditmemoItem) { + if ($creditmemoItem->getOrderItemId() == $orderItem->getItemId()) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php new file mode 100644 index 0000000000000..35b3b9feb3e3b --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php @@ -0,0 +1,105 @@ +returnProcessor = $returnProcessor; + $this->creditmemoRepository = $creditmemoRepository; + $this->orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @param \Magento\Sales\Api\RefundInvoiceInterface $refundService + * @param \Closure $proceed + * @param int $invoiceId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $isOnline + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExecute( + \Magento\Sales\Api\RefundInvoiceInterface $refundService, + \Closure $proceed, + $invoiceId, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $resultEntityId = $proceed($invoiceId, $items, $isOnline, $notify, $appendComment, $comment, $arguments); + if ($this->stockConfiguration->isAutoReturnEnabled()) { + return $resultEntityId; + } + + $invoice = $this->invoiceRepository->get($invoiceId); + $order = $this->orderRepository->get($invoice->getOrderId()); + + $returnToStockItems = []; + if ($arguments !== null + && $arguments->getExtensionAttributes() !== null + && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null + ) { + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + } + + $creditmemo = $this->creditmemoRepository->get($resultEntityId); + $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); + + return $resultEntityId; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php new file mode 100644 index 0000000000000..181411de7ccf1 --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php @@ -0,0 +1,101 @@ +returnProcessor = $returnProcessor; + $this->creditmemoRepository = $creditmemoRepository; + $this->orderRepository = $orderRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @param RefundOrderInterface $refundService + * @param \Closure $proceed + * @param int $orderId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExecute( + RefundOrderInterface $refundService, + \Closure $proceed, + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $resultEntityId = $proceed($orderId, $items, $notify, $appendComment, $comment,$arguments); + if ($this->stockConfiguration->isAutoReturnEnabled()) { + return $resultEntityId; + } + + $order = $this->orderRepository->get($orderId); + + $returnToStockItems = []; + if ($arguments !== null + && $arguments->getExtensionAttributes() !== null + && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null + ) { + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + } + + $creditmemo = $this->creditmemoRepository->get($resultEntityId); + $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); + + return $resultEntityId; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php new file mode 100644 index 0000000000000..5f9903f8be0a8 --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php @@ -0,0 +1,100 @@ +returnValidator = $returnValidator; + } + + /** + * @param RefundInvoiceInterface $refundInvoiceValidator + * @param \Closure $proceed + * @param InvoiceInterface $invoice + * @param OrderInterface $order + * @param CreditmemoInterface $creditmemo + * @param array $items + * @param bool $isOnline + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return ValidatorResultInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function aroundValidate( + RefundInvoiceInterface $refundInvoiceValidator, + \Closure $proceed, + InvoiceInterface $invoice, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $validationResults = $proceed( + $invoice, + $order, + $creditmemo, + $items, + $isOnline, + $notify, + $appendComment, + $comment, + $arguments + ); + if ($this->isReturnToStockItems($arguments)) { + return $validationResults; + } + + /** @var int[] $returnToStockItems */ + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); + if ($validationMessage) { + $validationResults->addMessage($validationMessage); + } + + return $validationResults; + } + + /** + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return bool + */ + private function isReturnToStockItems($arguments) + { + return $arguments === null + || $arguments->getExtensionAttributes() === null + || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php new file mode 100644 index 0000000000000..1f3239b5021b0 --- /dev/null +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php @@ -0,0 +1,84 @@ +returnValidator = $returnValidator; + } + + /** + * @param RefundOrderInterface $refundOrderValidator + * @param \Closure $proceed + * @param OrderInterface $order + * @param CreditmemoInterface $creditmemo + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @param CreditmemoCommentCreationInterface|null $comment + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return ValidatorResultInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundValidate( + RefundOrderInterface $refundOrderValidator, + \Closure $proceed, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $notify = false, + $appendComment = false, + CreditmemoCommentCreationInterface $comment = null, + CreditmemoCreationArgumentsInterface $arguments = null + ) { + $validationResults = $proceed($order, $creditmemo, $items, $notify, $appendComment, $comment, $arguments); + if ($this->isReturnToStockItems($arguments)) { + return $validationResults; + } + + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); + if ($validationMessage) { + $validationResults->addMessage($validationMessage); + } + + return $validationResults; + } + + /** + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return bool + */ + private function isReturnToStockItems($arguments) + { + return $arguments === null + || $arguments->getExtensionAttributes() === null + || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; + } +} diff --git a/app/code/Magento/SalesInventory/README.md b/app/code/Magento/SalesInventory/README.md new file mode 100644 index 0000000000000..094cbb838e5fe --- /dev/null +++ b/app/code/Magento/SalesInventory/README.md @@ -0,0 +1 @@ +Magento_SalesInventory module allows retrieve and update stock attributes related to Magento_Sales, such as status and quantity. diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php new file mode 100644 index 0000000000000..5497024d884c7 --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php @@ -0,0 +1,195 @@ +stockManagementMock = $this->getMockBuilder(StockManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockIndexerProcessorMock = $this->getMockBuilder( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class + )->disableOriginalConstructor() + ->getMock(); + $this->priceIndexerMock = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Price\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnProcessor = new ReturnProcessor( + $this->stockManagementMock, + $this->stockIndexerProcessorMock, + $this->priceIndexerMock, + $this->creditmemoRepositoryMock, + $this->storeManagerMock, + $this->orderRepositoryMock, + $this->orderItemRepositoryMock + ); + } + + public function testExecute() + { + $orderItemId = 99; + $productId = 50; + $returnToStockItems = [$orderItemId]; + $qty = 1; + $storeId = 0; + $webSiteId = 10; + + $this->creditmemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditmemoItemMock]); + + $this->creditmemoItemMock->expects($this->once()) + ->method('getQty') + ->willReturn($qty); + + $this->creditmemoItemMock->expects($this->exactly(2)) + ->method('getOrderItemId') + ->willReturn($orderItemId); + + $this->creditmemoItemMock->expects($this->once()) + ->method('getProductId') + ->willReturn($productId); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderItemId) + ->willReturn($this->orderItemMock); + + $this->orderMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with($storeId) + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->once()) + ->method('getWebsiteId') + ->willReturn($webSiteId); + + $this->stockManagementMock->expects($this->once()) + ->method('backItemQty') + ->with($productId, $qty, $webSiteId) + ->willReturn(true); + + $this->stockIndexerProcessorMock->expects($this->once()) + ->method('reindexList') + ->with([$productId]); + + $this->priceIndexerMock->expects($this->once()) + ->method('reindexList') + ->with([$productId]); + + $this->returnProcessor->execute($this->creditmemoMock, $this->orderMock, $returnToStockItems); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php new file mode 100644 index 0000000000000..9903881990cc8 --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php @@ -0,0 +1,134 @@ +orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditMemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditMemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnValidator = new ReturnValidator( + $this->orderItemRepositoryMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testValidate( + $expectedResult, + $returnToStockItems, + $orderItemId, + $creditMemoItemId, + $productSku = null + ) { + $this->creditMemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditMemoItemMock]); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($returnToStockItems[0]) + ->willReturn($this->orderItemMock); + + $this->orderItemMock->expects($this->once()) + ->method('getItemId') + ->willReturn($orderItemId); + + $this->creditMemoItemMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn($creditMemoItemId); + + if ($productSku) { + $this->orderItemMock->expects($this->once()) + ->method('getSku') + ->willReturn($productSku); + } + + $this->assertEquals( + $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), + $expectedResult + ); + } + + public function testValidationWithWrongOrderItems() + { + $returnToStockItems = [1]; + $this->creditMemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditMemoItemMock]); + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($returnToStockItems[0]) + ->willThrowException(new NoSuchEntityException); + + $this->assertEquals( + $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), + __('The return to stock argument contains product item that is not part of the original order.') + ); + + } + + public function dataProvider() + { + return [ + 'PostirivValidationTest' => [null, [1], 1, 1], + 'WithWrongReturnToStockItems' => [ + __('The "%1" product is not part of the current creditmemo.', 'sku1'), [2], 2, 1, 'sku1', + ], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php new file mode 100644 index 0000000000000..7f184b1081bf7 --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php @@ -0,0 +1,188 @@ +returnProcessorMock = $this->getMockBuilder(\Magento\SalesInventory\Model\Order\ReturnProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->refundInvoiceMock = $this->getMockBuilder(\Magento\Sales\Api\RefundInvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface::class + )->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface::class + )->disableOriginalConstructor() + ->setMethods(['getReturnToStockItems']) + ->getMock(); + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder( + \Magento\CatalogInventory\Api\StockConfigurationInterface::class + )->disableOriginalConstructor() + ->getMock(); + + $this->returnToStock = new \Magento\SalesInventory\Model\Plugin\Order\ReturnToStockInvoice( + $this->returnProcessorMock, + $this->creditmemoRepositoryMock, + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->stockConfigurationMock + ); + } + + public function testAroundExecute() + { + $orderId = 1; + $creditmemoId = 99; + $items = []; + $returnToStockItems = [1]; + $invoiceId = 98; + + $this->proceed = function () use ($creditmemoId) { + return $creditmemoId; + }; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->with($invoiceId) + ->willReturn($this->invoiceMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($this->orderMock); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + + $this->returnProcessorMock->expects($this->once()) + ->method('execute') + ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); + + $this->invoiceMock->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + + $this->stockConfigurationMock->expects($this->once()) + ->method('isAutoReturnEnabled') + ->willReturn(false); + + $this->assertEquals( + $this->returnToStock->aroundExecute( + $this->refundInvoiceMock, + $this->proceed, + $invoiceId, + $items, + false, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ), + $creditmemoId + ); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php new file mode 100644 index 0000000000000..19c2e04d06e1f --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php @@ -0,0 +1,166 @@ +returnProcessorMock = $this->getMockBuilder(ReturnProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->refundOrderMock = $this->getMockBuilder(RefundOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getReturnToStockItems']) + ->getMock(); + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnToStock = new ReturnToStockOrder( + $this->returnProcessorMock, + $this->creditmemoRepositoryMock, + $this->orderRepositoryMock, + $this->stockConfigurationMock + ); + } + + public function testAroundExecute() + { + $orderId = 1; + $creditmemoId = 99; + $items = []; + $returnToStockItems = [1]; + $this->proceed = function () use ($creditmemoId) { + return $creditmemoId; + }; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($this->orderMock); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + + $this->returnProcessorMock->expects($this->once()) + ->method('execute') + ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); + + $this->stockConfigurationMock->expects($this->once()) + ->method('isAutoReturnEnabled') + ->willReturn(false); + + $this->assertEquals( + $this->returnToStock->aroundExecute( + $this->refundOrderMock, + $this->proceed, + $orderId, + $items, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ), + $creditmemoId + ); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php new file mode 100644 index 0000000000000..a66268dee736b --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php @@ -0,0 +1,158 @@ +returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->setMethods(['getReturnToStockItems']) + ->disableOriginalConstructor() + ->getMock(); + + $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->refundInvoiceValidatorMock = $this->getMockBuilder(RefundInvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->proceed = function () { + return $this->validateResultMock; + }; + $this->plugin = new InvoiceRefundCreationArguments($this->returnValidatorMock); + } + + /** + * @dataProvider dataProvider + */ + public function testAroundValidation($errorMessage) + { + $returnToStockItems = [1]; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->returnValidatorMock->expects($this->once()) + ->method('validate') + ->willReturn($errorMessage); + + $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) + ->method('addMessage') + ->with($errorMessage); + + $this->plugin->aroundValidate( + $this->refundInvoiceValidatorMock, + $this->proceed, + $this->invoiceMock, + $this->orderMock, + $this->creditmemoMock, + [], + false, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'withErrors' => ['Error!'], + 'withoutErrors' => ['null'], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php new file mode 100644 index 0000000000000..a3f3212f1f6eb --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php @@ -0,0 +1,151 @@ +returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->setMethods(['getReturnToStockItems']) + ->disableOriginalConstructor() + ->getMock(); + + $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->refundOrderValidatorMock = $this->getMockBuilder(RefundOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->proceed = function () { + return $this->validateResultMock; + }; + + $this->plugin = new OrderRefundCreationArguments($this->returnValidatorMock); + } + + /** + * @dataProvider dataProvider + */ + public function testAroundValidation($errorMessage) + { + $returnToStockItems = [1]; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->returnValidatorMock->expects($this->once()) + ->method('validate') + ->willReturn($errorMessage); + + $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) + ->method('addMessage') + ->with($errorMessage); + + $this->plugin->aroundValidate( + $this->refundOrderValidatorMock, + $this->proceed, + $this->orderMock, + $this->creditmemoMock, + [], + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ); + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + 'withErrors' => ['Error!'], + 'withoutErrors' => ['null'], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/composer.json b/app/code/Magento/SalesInventory/composer.json new file mode 100644 index 0000000000000..4008a220fd844 --- /dev/null +++ b/app/code/Magento/SalesInventory/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-sales-inventory", + "description": "N/A", + "require": { + "php": "~5.6.0|7.0.2|~7.0.6", + "magento/module-catalog-inventory": "100.1.*", + "magento/module-sales": "100.1.*", + "magento/module-store": "100.1.*", + "magento/module-catalog": "101.0.*", + "magento/framework": "100.1.*" + }, + "type": "magento2-module", + "version": "100.0.0", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SalesInventory\\": "" + } + } +} diff --git a/app/code/Magento/SalesInventory/etc/di.xml b/app/code/Magento/SalesInventory/etc/di.xml new file mode 100644 index 0000000000000..7daff0c2d7e53 --- /dev/null +++ b/app/code/Magento/SalesInventory/etc/di.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/SalesInventory/etc/extension_attributes.xml b/app/code/Magento/SalesInventory/etc/extension_attributes.xml new file mode 100644 index 0000000000000..5c794fe101e33 --- /dev/null +++ b/app/code/Magento/SalesInventory/etc/extension_attributes.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/SalesInventory/etc/module.xml b/app/code/Magento/SalesInventory/etc/module.xml new file mode 100644 index 0000000000000..e475dd103b0cb --- /dev/null +++ b/app/code/Magento/SalesInventory/etc/module.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/SalesInventory/registration.php b/app/code/Magento/SalesInventory/registration.php new file mode 100644 index 0000000000000..bd4ecdbd48803 --- /dev/null +++ b/app/code/Magento/SalesInventory/registration.php @@ -0,0 +1,11 @@ +_objectManager->get('Magento\Backend\Model\Session')->setCommentText($data['comment_text']); } + $isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label']; + try { $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); @@ -129,10 +131,12 @@ public function execute() $shipment->setCustomerNote($data['comment_text']); $shipment->setCustomerNoteNotify(isset($data['comment_customer_notify'])); } - $errorMessages = $this->getShipmentValidator()->validate($shipment, [QuantityValidator::class]); - if (!empty($errorMessages)) { + $validationResult = $this->getShipmentValidator() + ->validate($shipment, [QuantityValidator::class]); + + if ($validationResult->hasMessages()) { $this->messageManager->addError( - __("Shipment Document Validation Error(s):\n" . implode("\n", $errorMessages)) + __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); return; @@ -141,7 +145,6 @@ public function execute() $shipment->getOrder()->setCustomerNoteNotify(!empty($data['send_email'])); $responseAjax = new \Magento\Framework\DataObject(); - $isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label']; if ($isNeedCreateLabel) { $this->labelGenerator->create($shipment, $this->_request); diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index f34c90e5b9ff0..7afa5771b157f 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -9,6 +9,7 @@ namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; +use Magento\Sales\Model\ValidatorResultInterface; use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface; @@ -18,6 +19,7 @@ * Class SaveTest * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class SaveTest extends \PHPUnit_Framework_TestCase { @@ -96,6 +98,11 @@ class SaveTest extends \PHPUnit_Framework_TestCase */ private $shipmentValidatorMock; + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validationResult; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -107,6 +114,9 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods([]) ->getMock(); + $this->validationResult = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->labelGenerator = $this->getMockBuilder(\Magento\Shipping\Model\Shipping\LabelGenerator::class) ->disableOriginalConstructor() ->setMethods([]) @@ -363,7 +373,11 @@ public function testExecute($formKeyIsValid, $isPost) $this->shipmentValidatorMock->expects($this->once()) ->method('validate') ->with($shipment, [QuantityValidator::class]) - ->willReturn([]); + ->willReturn($this->validationResult); + + $this->validationResult->expects($this->once()) + ->method('hasMessages') + ->willReturn(false); $this->saveAction->execute(); $this->assertEquals($this->response, $this->saveAction->getResponse()); diff --git a/composer.json b/composer.json index 832e806584189..c2d26f0e8b594 100644 --- a/composer.json +++ b/composer.json @@ -151,6 +151,7 @@ "magento/module-rss": "100.0.5", "magento/module-rule": "100.0.5", "magento/module-sales": "100.0.9", + "magento/module-sales-inventory": "100.0.0", "magento/module-sales-rule": "100.0.6", "magento/module-sales-sequence": "100.0.5", "magento/module-sample-data": "100.0.5", diff --git a/dev/tests/api-functional/testsuite/Magento/SalesInventory/Api/Service/V1/ReturnItemsAfterRefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/SalesInventory/Api/Service/V1/ReturnItemsAfterRefundOrderTest.php new file mode 100644 index 0000000000000..a7d35f4782586 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/SalesInventory/Api/Service/V1/ReturnItemsAfterRefundOrderTest.php @@ -0,0 +1,114 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @dataProvider dataProvider + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testRefundWithReturnItemsToStock($qtyRefund) + { + $productSku = 'simple'; + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + $orderItems = $existingOrder->getItems(); + $orderItem = array_shift($orderItems); + $expectedItems = [['order_item_id' => $orderItem->getItemId(), 'qty' => $qtyRefund]]; + $qtyBeforeRefund = $this->getQtyInStockBySku($productSku); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getEntityId() . '/refund', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_REFUND_ORDER_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_REFUND_ORDER_NAME . 'execute', + ] + ]; + + $this->_webApiCall( + $serviceInfo, + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $expectedItems, + 'arguments' => [ + 'extension_attributes' => [ + 'return_to_stock_items' => [ + (int)$orderItem->getItemId() + ], + ], + ], + ] + ); + + $qtyAfterRefund = $this->getQtyInStockBySku($productSku); + + try { + $this->assertEquals( + $qtyBeforeRefund + $expectedItems[0]['qty'], + $qtyAfterRefund, + 'Failed asserting qty of returned items incorrect.' + ); + + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + 'refundAllOrderItems' => [2], + 'refundPartition' => [1], + ]; + } + + /** + * @param string $sku + * @return int + */ + private function getQtyInStockBySku($sku) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/' . self::SERVICE_STOCK_ITEMS_NAME . "/$sku", + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'catalogInventoryStockRegistryV1', + 'serviceVersion' => 'V1', + 'operation' => 'catalogInventoryStockRegistryV1GetStockItemBySku', + ], + ]; + $arguments = ['productSku' => $sku]; + $apiResult = $this->_webApiCall($serviceInfo, $arguments); + return $apiResult['qty']; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php new file mode 100644 index 0000000000000..c57f53582870c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php @@ -0,0 +1,6 @@ + Date: Mon, 10 Oct 2016 15:55:53 +0300 Subject: [PATCH 22/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- .../Model/Order/Validation/InvoiceOrder.php | 158 +++---- .../Validation/InvoiceOrderInterface.php | 84 ++-- .../Model/Order/Validation/RefundInvoice.php | 246 +++++------ .../Validation/RefundInvoiceInterface.php | 86 ++-- .../Model/Order/Validation/RefundOrder.php | 208 +++++----- .../Order/Validation/RefundOrderInterface.php | 76 ++-- .../Model/Order/Validation/ShipOrder.php | 184 ++++----- .../Order/Validation/ShipOrderInterface.php | 82 ++-- .../Magento/Sales/Model/ValidatorResult.php | 82 ++-- .../Sales/Model/ValidatorResultInterface.php | 58 +-- .../Sales/Model/ValidatorResultMerger.php | 92 ++--- .../Model/Order/ReturnProcessor.php | 312 +++++++------- .../Model/Order/ReturnValidator.php | 140 +++---- .../Plugin/Order/ReturnToStockInvoice.php | 210 +++++----- .../Model/Plugin/Order/ReturnToStockOrder.php | 202 ++++----- .../InvoiceRefundCreationArguments.php | 200 ++++----- .../OrderRefundCreationArguments.php | 168 ++++---- .../Unit/Model/Order/ReturnProcessorTest.php | 390 +++++++++--------- .../Unit/Model/Order/ReturnValidatorTest.php | 268 ++++++------ .../Plugin/Order/ReturnToStockInvoiceTest.php | 376 ++++++++--------- .../Plugin/Order/ReturnToStockOrderTest.php | 332 +++++++-------- .../InvoiceRefundCreationArgumentsTest.php | 316 +++++++------- .../OrderRefundCreationArgumentsTest.php | 302 +++++++------- .../Magento/SalesInventory/registration.php | 22 +- .../V1/ReturnItemsAfterRefundOrderTest.php | 228 +++++----- ...der_with_shipping_and_invoice_rollback.php | 12 +- 26 files changed, 2417 insertions(+), 2417 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php index c0882bfd37253..d912793afa157 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php +++ b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrder.php @@ -1,79 +1,79 @@ -invoiceValidator = $invoiceValidator; - $this->orderValidator = $orderValidator; - $this->validatorResultMerger = $validatorResultMerger; - } - - /** - * @inheritdoc - */ - public function validate( - OrderInterface $order, - InvoiceInterface $invoice, - $capture = false, - array $items = [], - $notify = false, - $appendComment = false, - InvoiceCommentCreationInterface $comment = null, - InvoiceCreationArgumentsInterface $arguments = null - ) { - return $this->validatorResultMerger->merge( - $this->invoiceValidator->validate( - $invoice, - [InvoiceQuantityValidator::class] - ), - $this->orderValidator->validate( - $order, - [CanInvoice::class] - ) - ); - } -} +invoiceValidator = $invoiceValidator; + $this->orderValidator = $orderValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + OrderInterface $order, + InvoiceInterface $invoice, + $capture = false, + array $items = [], + $notify = false, + $appendComment = false, + InvoiceCommentCreationInterface $comment = null, + InvoiceCreationArgumentsInterface $arguments = null + ) { + return $this->validatorResultMerger->merge( + $this->invoiceValidator->validate( + $invoice, + [InvoiceQuantityValidator::class] + ), + $this->orderValidator->validate( + $order, + [CanInvoice::class] + ) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php index 92cf617846087..5c27741a19855 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php +++ b/app/code/Magento/Sales/Model/Order/Validation/InvoiceOrderInterface.php @@ -1,42 +1,42 @@ -orderValidator = $orderValidator; - $this->creditmemoValidator = $creditmemoValidator; - $this->itemCreationValidator = $itemCreationValidator; - $this->invoiceValidator = $invoiceValidator; - $this->validatorResultMerger = $validatorResultMerger; - } - - /** - * @inheritdoc - */ - public function validate( - InvoiceInterface $invoice, - OrderInterface $order, - CreditmemoInterface $creditmemo, - array $items = [], - $isOnline = false, - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null - ) { - $orderValidationResult = $this->orderValidator->validate( - $order, - [ - CanRefund::class - ] - ); - $creditmemoValidationResult = $this->creditmemoValidator->validate( - $creditmemo, - [ - QuantityValidator::class, - TotalsValidator::class - ] - ); - - $itemsValidation = []; - foreach ($items as $item) { - $itemsValidation[] = $this->itemCreationValidator->validate( - $item, - [CreationQuantityValidator::class], - $order - )->getMessages(); - } - - $invoiceValidationResult = $this->invoiceValidator->validate( - $invoice, - [ - \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class - ] - ); - - return $this->validatorResultMerger->merge( - $orderValidationResult, - $creditmemoValidationResult, - $invoiceValidationResult->getMessages(), - ...array_values($itemsValidation) - ); - } -} +orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->invoiceValidator = $invoiceValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + InvoiceInterface $invoice, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation[] = $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + )->getMessages(); + } + + $invoiceValidationResult = $this->invoiceValidator->validate( + $invoice, + [ + \Magento\Sales\Model\Order\Invoice\Validation\CanRefund::class + ] + ); + + return $this->validatorResultMerger->merge( + $orderValidationResult, + $creditmemoValidationResult, + $invoiceValidationResult->getMessages(), + ...array_values($itemsValidation) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php index a6bd5e3dbc807..83acc9811bb89 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoiceInterface.php @@ -1,43 +1,43 @@ -orderValidator = $orderValidator; - $this->creditmemoValidator = $creditmemoValidator; - $this->itemCreationValidator = $itemCreationValidator; - $this->validatorResultMerger = $validatorResultMerger; - } - - /** - * @inheritdoc - */ - public function validate( - OrderInterface $order, - CreditmemoInterface $creditmemo, - array $items = [], - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null - ) { - $orderValidationResult = $this->orderValidator->validate( - $order, - [ - CanRefund::class - ] - ); - $creditmemoValidationResult = $this->creditmemoValidator->validate( - $creditmemo, - [ - QuantityValidator::class, - TotalsValidator::class - ] - ); - - $itemsValidation = []; - foreach ($items as $item) { - $itemsValidation[] = $this->itemCreationValidator->validate( - $item, - [CreationQuantityValidator::class], - $order - )->getMessages(); - } - - return $this->validatorResultMerger->merge( - $orderValidationResult, - $creditmemoValidationResult, - ...array_values($itemsValidation) - ); - } -} +orderValidator = $orderValidator; + $this->creditmemoValidator = $creditmemoValidator; + $this->itemCreationValidator = $itemCreationValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @inheritdoc + */ + public function validate( + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanRefund::class + ] + ); + $creditmemoValidationResult = $this->creditmemoValidator->validate( + $creditmemo, + [ + QuantityValidator::class, + TotalsValidator::class + ] + ); + + $itemsValidation = []; + foreach ($items as $item) { + $itemsValidation[] = $this->itemCreationValidator->validate( + $item, + [CreationQuantityValidator::class], + $order + )->getMessages(); + } + + return $this->validatorResultMerger->merge( + $orderValidationResult, + $creditmemoValidationResult, + ...array_values($itemsValidation) + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php index dc43b470d0d1b..2f770d20b5134 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundOrderInterface.php @@ -1,38 +1,38 @@ -orderValidator = $orderValidator; - $this->shipmentValidator = $shipmentValidator; - $this->validatorResultMerger = $validatorResultMerger; - } - - /** - * @param OrderInterface $order - * @param ShipmentInterface $shipment - * @param array $items - * @param bool $notify - * @param bool $appendComment - * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment - * @param array $tracks - * @param array $packages - * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments - * @return \Magento\Sales\Model\ValidatorResultInterface - */ - public function validate( - $order, - $shipment, - array $items = [], - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, - array $tracks = [], - array $packages = [], - \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null - ) { - $orderValidationResult = $this->orderValidator->validate( - $order, - [ - CanShip::class - ] - ); - $shipmentValidationResult = $this->shipmentValidator->validate( - $shipment, - [ - QuantityValidator::class, - TrackValidator::class - ] - ); - - return $this->validatorResultMerger->merge($orderValidationResult, $shipmentValidationResult); - } -} +orderValidator = $orderValidator; + $this->shipmentValidator = $shipmentValidator; + $this->validatorResultMerger = $validatorResultMerger; + } + + /** + * @param OrderInterface $order + * @param ShipmentInterface $shipment + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param array $tracks + * @param array $packages + * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments + * @return \Magento\Sales\Model\ValidatorResultInterface + */ + public function validate( + $order, + $shipment, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + array $tracks = [], + array $packages = [], + \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null + ) { + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanShip::class + ] + ); + $shipmentValidationResult = $this->shipmentValidator->validate( + $shipment, + [ + QuantityValidator::class, + TrackValidator::class + ] + ); + + return $this->validatorResultMerger->merge($orderValidationResult, $shipmentValidationResult); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php b/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php index e3760c4dc9604..43f12df445b79 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php +++ b/app/code/Magento/Sales/Model/Order/Validation/ShipOrderInterface.php @@ -1,41 +1,41 @@ -messages[] = (string)$message; - } - - /** - * @return bool - */ - public function hasMessages() - { - return count($this->messages) > 0; - } - - /** - * @return string[] - */ - public function getMessages() - { - return $this->messages; - } -} +messages[] = (string)$message; + } + + /** + * @return bool + */ + public function hasMessages() + { + return count($this->messages) > 0; + } + + /** + * @return string[] + */ + public function getMessages() + { + return $this->messages; + } +} diff --git a/app/code/Magento/Sales/Model/ValidatorResultInterface.php b/app/code/Magento/Sales/Model/ValidatorResultInterface.php index 2de57cb9f1485..c072d30e2f93f 100644 --- a/app/code/Magento/Sales/Model/ValidatorResultInterface.php +++ b/app/code/Magento/Sales/Model/ValidatorResultInterface.php @@ -1,29 +1,29 @@ -validatorResultInterfaceFactory = $validatorResultInterfaceFactory; - } - - /** - * Merge two validator results and additional messages - * - * @param ValidatorResultInterface $first - * @param ValidatorResultInterface $second - * @return ValidatorResultInterface - */ - public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second) - { - $messages = array_merge($first->getMessages(), $second->getMessages(), ...array_slice(func_get_args(), 2)); - - $result = $this->validatorResultInterfaceFactory->create(); - foreach ($messages as $message) { - $result->addMessage($message); - } - - return $result; - } -} +validatorResultInterfaceFactory = $validatorResultInterfaceFactory; + } + + /** + * Merge two validator results and additional messages + * + * @param ValidatorResultInterface $first + * @param ValidatorResultInterface $second + * @return ValidatorResultInterface + */ + public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second) + { + $messages = array_merge($first->getMessages(), $second->getMessages(), ...array_slice(func_get_args(), 2)); + + $result = $this->validatorResultInterfaceFactory->create(); + foreach ($messages as $message) { + $result->addMessage($message); + } + + return $result; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php index 06ac2e2de7636..7752d7131031c 100644 --- a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php @@ -1,156 +1,156 @@ -stockManagement = $stockManagement; - $this->stockIndexerProcessor = $stockIndexer; - $this->priceIndexer = $priceIndexer; - $this->creditmemoRepository = $creditmemoRepository; - $this->storeManager = $storeManager; - $this->orderRepository = $orderRepository; - $this->orderItemRepository = $orderItemRepository; - } - - /** - * @param CreditmemoInterface $creditmemo - * @param OrderInterface $order - * @param array $returnToStockItems - * @return void - */ - public function execute( - CreditmemoInterface $creditmemo, - OrderInterface $order, - array $returnToStockItems = [] - ) { - $itemsToUpdate = []; - foreach ($creditmemo->getItems() as $item) { - $qty = $item->getQty(); - $productId = $item->getProductId(); - $orderItem = $this->orderItemRepository->get($item->getOrderItemId()); - $parentItemId = $orderItem->getParentItemId(); - if ($this->canReturnItem($item, $qty, $parentItemId, $returnToStockItems)) { - $parentItem = $parentItemId ? $this->getItemByOrderId($creditmemo, $parentItemId) : false; - $qty = $parentItem ? $parentItem->getQty() * $qty : $qty; - if (isset($itemsToUpdate[$productId])) { - $itemsToUpdate[$productId] += $qty; - } else { - $itemsToUpdate[$productId] = $qty; - } - } - } - - if (!empty($itemsToUpdate)) { - $store = $this->storeManager->getStore($order->getStoreId()); - foreach ($itemsToUpdate as $productId => $qty) { - $this->stockManagement->backItemQty( - $productId, - $qty, - $store->getWebsiteId() - ); - } - - $updatedItemIds = array_keys($itemsToUpdate); - $this->stockIndexerProcessor->reindexList($updatedItemIds); - $this->priceIndexer->reindexList($updatedItemIds); - } - } - - /** - * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo - * @param int $parentItemId - * @return bool|CreditmemoItemInterface - */ - private function getItemByOrderId(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, $parentItemId) - { - foreach ($creditmemo->getItems() as $item) { - if ($item->getOrderItemId() == $parentItemId) { - return $item; - } - } - return false; - } - - /** - * @param \Magento\Sales\Api\Data\CreditmemoItemInterface $item - * @param int $qty - * @param int[] $returnToStockItems - * @param int $parentItemId - * @return bool - */ - private function canReturnItem( - \Magento\Sales\Api\Data\CreditmemoItemInterface $item, - $qty, - $parentItemId = null, - array $returnToStockItems = [] - ) { - return (in_array($item->getOrderItemId(), $returnToStockItems) || in_array($parentItemId, $returnToStockItems)) - && $qty; - } -} +stockManagement = $stockManagement; + $this->stockIndexerProcessor = $stockIndexer; + $this->priceIndexer = $priceIndexer; + $this->creditmemoRepository = $creditmemoRepository; + $this->storeManager = $storeManager; + $this->orderRepository = $orderRepository; + $this->orderItemRepository = $orderItemRepository; + } + + /** + * @param CreditmemoInterface $creditmemo + * @param OrderInterface $order + * @param array $returnToStockItems + * @return void + */ + public function execute( + CreditmemoInterface $creditmemo, + OrderInterface $order, + array $returnToStockItems = [] + ) { + $itemsToUpdate = []; + foreach ($creditmemo->getItems() as $item) { + $qty = $item->getQty(); + $productId = $item->getProductId(); + $orderItem = $this->orderItemRepository->get($item->getOrderItemId()); + $parentItemId = $orderItem->getParentItemId(); + if ($this->canReturnItem($item, $qty, $parentItemId, $returnToStockItems)) { + $parentItem = $parentItemId ? $this->getItemByOrderId($creditmemo, $parentItemId) : false; + $qty = $parentItem ? $parentItem->getQty() * $qty : $qty; + if (isset($itemsToUpdate[$productId])) { + $itemsToUpdate[$productId] += $qty; + } else { + $itemsToUpdate[$productId] = $qty; + } + } + } + + if (!empty($itemsToUpdate)) { + $store = $this->storeManager->getStore($order->getStoreId()); + foreach ($itemsToUpdate as $productId => $qty) { + $this->stockManagement->backItemQty( + $productId, + $qty, + $store->getWebsiteId() + ); + } + + $updatedItemIds = array_keys($itemsToUpdate); + $this->stockIndexerProcessor->reindexList($updatedItemIds); + $this->priceIndexer->reindexList($updatedItemIds); + } + } + + /** + * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo + * @param int $parentItemId + * @return bool|CreditmemoItemInterface + */ + private function getItemByOrderId(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, $parentItemId) + { + foreach ($creditmemo->getItems() as $item) { + if ($item->getOrderItemId() == $parentItemId) { + return $item; + } + } + return false; + } + + /** + * @param \Magento\Sales\Api\Data\CreditmemoItemInterface $item + * @param int $qty + * @param int[] $returnToStockItems + * @param int $parentItemId + * @return bool + */ + private function canReturnItem( + \Magento\Sales\Api\Data\CreditmemoItemInterface $item, + $qty, + $parentItemId = null, + array $returnToStockItems = [] + ) { + return (in_array($item->getOrderItemId(), $returnToStockItems) || in_array($parentItemId, $returnToStockItems)) + && $qty; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php b/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php index 85ee15e2ce6ac..2195883334fcf 100644 --- a/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnValidator.php @@ -1,70 +1,70 @@ -orderItemRepository = $orderItemRepository; - } - - /** - * @param int[] $returnToStockItems - * @param CreditmemoInterface $creditmemo - * @return \Magento\Framework\Phrase|null - */ - public function validate($returnToStockItems, CreditmemoInterface $creditmemo) - { - $creditmemoItems = $creditmemo->getItems(); - - /** @var int $item */ - foreach ($returnToStockItems as $item) { - try { - $orderItem = $this->orderItemRepository->get($item); - if (!$this->isOrderItemPartOfCreditmemo($creditmemoItems, $orderItem)) { - return __('The "%1" product is not part of the current creditmemo.', $orderItem->getSku()); - } - } catch (NoSuchEntityException $e) { - return __('The return to stock argument contains product item that is not part of the original order.'); - } - } - return null; - } - - /** - * @param CreditmemoItemInterface[] $creditmemoItems - * @param OrderItemInterface $orderItem - * @return bool - */ - private function isOrderItemPartOfCreditmemo(array $creditmemoItems, OrderItemInterface $orderItem) - { - foreach ($creditmemoItems as $creditmemoItem) { - if ($creditmemoItem->getOrderItemId() == $orderItem->getItemId()) { - return true; - } - } - return false; - } -} +orderItemRepository = $orderItemRepository; + } + + /** + * @param int[] $returnToStockItems + * @param CreditmemoInterface $creditmemo + * @return \Magento\Framework\Phrase|null + */ + public function validate($returnToStockItems, CreditmemoInterface $creditmemo) + { + $creditmemoItems = $creditmemo->getItems(); + + /** @var int $item */ + foreach ($returnToStockItems as $item) { + try { + $orderItem = $this->orderItemRepository->get($item); + if (!$this->isOrderItemPartOfCreditmemo($creditmemoItems, $orderItem)) { + return __('The "%1" product is not part of the current creditmemo.', $orderItem->getSku()); + } + } catch (NoSuchEntityException $e) { + return __('The return to stock argument contains product item that is not part of the original order.'); + } + } + return null; + } + + /** + * @param CreditmemoItemInterface[] $creditmemoItems + * @param OrderItemInterface $orderItem + * @return bool + */ + private function isOrderItemPartOfCreditmemo(array $creditmemoItems, OrderItemInterface $orderItem) + { + foreach ($creditmemoItems as $creditmemoItem) { + if ($creditmemoItem->getOrderItemId() == $orderItem->getItemId()) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php index 35b3b9feb3e3b..02f258d02d65b 100644 --- a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockInvoice.php @@ -1,105 +1,105 @@ -returnProcessor = $returnProcessor; - $this->creditmemoRepository = $creditmemoRepository; - $this->orderRepository = $orderRepository; - $this->invoiceRepository = $invoiceRepository; - $this->stockConfiguration = $stockConfiguration; - } - - /** - * @param \Magento\Sales\Api\RefundInvoiceInterface $refundService - * @param \Closure $proceed - * @param int $invoiceId - * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items - * @param bool|null $isOnline - * @param bool|null $notify - * @param bool|null $appendComment - * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment - * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments - * @return int - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundExecute( - \Magento\Sales\Api\RefundInvoiceInterface $refundService, - \Closure $proceed, - $invoiceId, - array $items = [], - $isOnline = false, - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null - ) { - $resultEntityId = $proceed($invoiceId, $items, $isOnline, $notify, $appendComment, $comment, $arguments); - if ($this->stockConfiguration->isAutoReturnEnabled()) { - return $resultEntityId; - } - - $invoice = $this->invoiceRepository->get($invoiceId); - $order = $this->orderRepository->get($invoice->getOrderId()); - - $returnToStockItems = []; - if ($arguments !== null - && $arguments->getExtensionAttributes() !== null - && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null - ) { - $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); - } - - $creditmemo = $this->creditmemoRepository->get($resultEntityId); - $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); - - return $resultEntityId; - } -} +returnProcessor = $returnProcessor; + $this->creditmemoRepository = $creditmemoRepository; + $this->orderRepository = $orderRepository; + $this->invoiceRepository = $invoiceRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @param \Magento\Sales\Api\RefundInvoiceInterface $refundService + * @param \Closure $proceed + * @param int $invoiceId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $isOnline + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExecute( + \Magento\Sales\Api\RefundInvoiceInterface $refundService, + \Closure $proceed, + $invoiceId, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $resultEntityId = $proceed($invoiceId, $items, $isOnline, $notify, $appendComment, $comment, $arguments); + if ($this->stockConfiguration->isAutoReturnEnabled()) { + return $resultEntityId; + } + + $invoice = $this->invoiceRepository->get($invoiceId); + $order = $this->orderRepository->get($invoice->getOrderId()); + + $returnToStockItems = []; + if ($arguments !== null + && $arguments->getExtensionAttributes() !== null + && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null + ) { + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + } + + $creditmemo = $this->creditmemoRepository->get($resultEntityId); + $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); + + return $resultEntityId; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php index 181411de7ccf1..58d0c19aea938 100644 --- a/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/ReturnToStockOrder.php @@ -1,101 +1,101 @@ -returnProcessor = $returnProcessor; - $this->creditmemoRepository = $creditmemoRepository; - $this->orderRepository = $orderRepository; - $this->stockConfiguration = $stockConfiguration; - } - - /** - * @param RefundOrderInterface $refundService - * @param \Closure $proceed - * @param int $orderId - * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items - * @param bool|null $notify - * @param bool|null $appendComment - * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment - * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments - * @return int - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundExecute( - RefundOrderInterface $refundService, - \Closure $proceed, - $orderId, - array $items = [], - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null - ) { - $resultEntityId = $proceed($orderId, $items, $notify, $appendComment, $comment,$arguments); - if ($this->stockConfiguration->isAutoReturnEnabled()) { - return $resultEntityId; - } - - $order = $this->orderRepository->get($orderId); - - $returnToStockItems = []; - if ($arguments !== null - && $arguments->getExtensionAttributes() !== null - && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null - ) { - $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); - } - - $creditmemo = $this->creditmemoRepository->get($resultEntityId); - $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); - - return $resultEntityId; - } -} +returnProcessor = $returnProcessor; + $this->creditmemoRepository = $creditmemoRepository; + $this->orderRepository = $orderRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @param RefundOrderInterface $refundService + * @param \Closure $proceed + * @param int $orderId + * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items + * @param bool|null $notify + * @param bool|null $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return int + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExecute( + RefundOrderInterface $refundService, + \Closure $proceed, + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $resultEntityId = $proceed($orderId, $items, $notify, $appendComment, $comment,$arguments); + if ($this->stockConfiguration->isAutoReturnEnabled()) { + return $resultEntityId; + } + + $order = $this->orderRepository->get($orderId); + + $returnToStockItems = []; + if ($arguments !== null + && $arguments->getExtensionAttributes() !== null + && $arguments->getExtensionAttributes()->getReturnToStockItems() !== null + ) { + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + } + + $creditmemo = $this->creditmemoRepository->get($resultEntityId); + $this->returnProcessor->execute($creditmemo, $order, $returnToStockItems); + + return $resultEntityId; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php index 5f9903f8be0a8..fbf42ae6e0b1f 100644 --- a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/InvoiceRefundCreationArguments.php @@ -1,100 +1,100 @@ -returnValidator = $returnValidator; - } - - /** - * @param RefundInvoiceInterface $refundInvoiceValidator - * @param \Closure $proceed - * @param InvoiceInterface $invoice - * @param OrderInterface $order - * @param CreditmemoInterface $creditmemo - * @param array $items - * @param bool $isOnline - * @param bool $notify - * @param bool $appendComment - * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment - * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments - * @return ValidatorResultInterface - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function aroundValidate( - RefundInvoiceInterface $refundInvoiceValidator, - \Closure $proceed, - InvoiceInterface $invoice, - OrderInterface $order, - CreditmemoInterface $creditmemo, - array $items = [], - $isOnline = false, - $notify = false, - $appendComment = false, - \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null - ) { - $validationResults = $proceed( - $invoice, - $order, - $creditmemo, - $items, - $isOnline, - $notify, - $appendComment, - $comment, - $arguments - ); - if ($this->isReturnToStockItems($arguments)) { - return $validationResults; - } - - /** @var int[] $returnToStockItems */ - $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); - $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); - if ($validationMessage) { - $validationResults->addMessage($validationMessage); - } - - return $validationResults; - } - - /** - * @param CreditmemoCreationArgumentsInterface|null $arguments - * @return bool - */ - private function isReturnToStockItems($arguments) - { - return $arguments === null - || $arguments->getExtensionAttributes() === null - || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; - } -} +returnValidator = $returnValidator; + } + + /** + * @param RefundInvoiceInterface $refundInvoiceValidator + * @param \Closure $proceed + * @param InvoiceInterface $invoice + * @param OrderInterface $order + * @param CreditmemoInterface $creditmemo + * @param array $items + * @param bool $isOnline + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface|null $arguments + * @return ValidatorResultInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function aroundValidate( + RefundInvoiceInterface $refundInvoiceValidator, + \Closure $proceed, + InvoiceInterface $invoice, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $isOnline = false, + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface $arguments = null + ) { + $validationResults = $proceed( + $invoice, + $order, + $creditmemo, + $items, + $isOnline, + $notify, + $appendComment, + $comment, + $arguments + ); + if ($this->isReturnToStockItems($arguments)) { + return $validationResults; + } + + /** @var int[] $returnToStockItems */ + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); + if ($validationMessage) { + $validationResults->addMessage($validationMessage); + } + + return $validationResults; + } + + /** + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return bool + */ + private function isReturnToStockItems($arguments) + { + return $arguments === null + || $arguments->getExtensionAttributes() === null + || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; + } +} diff --git a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php index 1f3239b5021b0..8a61fb01366fb 100644 --- a/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php +++ b/app/code/Magento/SalesInventory/Model/Plugin/Order/Validation/OrderRefundCreationArguments.php @@ -1,84 +1,84 @@ -returnValidator = $returnValidator; - } - - /** - * @param RefundOrderInterface $refundOrderValidator - * @param \Closure $proceed - * @param OrderInterface $order - * @param CreditmemoInterface $creditmemo - * @param array $items - * @param bool $notify - * @param bool $appendComment - * @param CreditmemoCommentCreationInterface|null $comment - * @param CreditmemoCreationArgumentsInterface|null $arguments - * @return ValidatorResultInterface - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundValidate( - RefundOrderInterface $refundOrderValidator, - \Closure $proceed, - OrderInterface $order, - CreditmemoInterface $creditmemo, - array $items = [], - $notify = false, - $appendComment = false, - CreditmemoCommentCreationInterface $comment = null, - CreditmemoCreationArgumentsInterface $arguments = null - ) { - $validationResults = $proceed($order, $creditmemo, $items, $notify, $appendComment, $comment, $arguments); - if ($this->isReturnToStockItems($arguments)) { - return $validationResults; - } - - $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); - $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); - if ($validationMessage) { - $validationResults->addMessage($validationMessage); - } - - return $validationResults; - } - - /** - * @param CreditmemoCreationArgumentsInterface|null $arguments - * @return bool - */ - private function isReturnToStockItems($arguments) - { - return $arguments === null - || $arguments->getExtensionAttributes() === null - || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; - } -} +returnValidator = $returnValidator; + } + + /** + * @param RefundOrderInterface $refundOrderValidator + * @param \Closure $proceed + * @param OrderInterface $order + * @param CreditmemoInterface $creditmemo + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @param CreditmemoCommentCreationInterface|null $comment + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return ValidatorResultInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundValidate( + RefundOrderInterface $refundOrderValidator, + \Closure $proceed, + OrderInterface $order, + CreditmemoInterface $creditmemo, + array $items = [], + $notify = false, + $appendComment = false, + CreditmemoCommentCreationInterface $comment = null, + CreditmemoCreationArgumentsInterface $arguments = null + ) { + $validationResults = $proceed($order, $creditmemo, $items, $notify, $appendComment, $comment, $arguments); + if ($this->isReturnToStockItems($arguments)) { + return $validationResults; + } + + $returnToStockItems = $arguments->getExtensionAttributes()->getReturnToStockItems(); + $validationMessage = $this->returnValidator->validate($returnToStockItems, $creditmemo); + if ($validationMessage) { + $validationResults->addMessage($validationMessage); + } + + return $validationResults; + } + + /** + * @param CreditmemoCreationArgumentsInterface|null $arguments + * @return bool + */ + private function isReturnToStockItems($arguments) + { + return $arguments === null + || $arguments->getExtensionAttributes() === null + || $arguments->getExtensionAttributes()->getReturnToStockItems() === null; + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php index 5497024d884c7..523759d54645a 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnProcessorTest.php @@ -1,195 +1,195 @@ -stockManagementMock = $this->getMockBuilder(StockManagementInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->stockIndexerProcessorMock = $this->getMockBuilder( - \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class - )->disableOriginalConstructor() - ->getMock(); - $this->priceIndexerMock = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Price\Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderMock = $this->getMockBuilder(OrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeMock = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->returnProcessor = new ReturnProcessor( - $this->stockManagementMock, - $this->stockIndexerProcessorMock, - $this->priceIndexerMock, - $this->creditmemoRepositoryMock, - $this->storeManagerMock, - $this->orderRepositoryMock, - $this->orderItemRepositoryMock - ); - } - - public function testExecute() - { - $orderItemId = 99; - $productId = 50; - $returnToStockItems = [$orderItemId]; - $qty = 1; - $storeId = 0; - $webSiteId = 10; - - $this->creditmemoMock->expects($this->once()) - ->method('getItems') - ->willReturn([$this->creditmemoItemMock]); - - $this->creditmemoItemMock->expects($this->once()) - ->method('getQty') - ->willReturn($qty); - - $this->creditmemoItemMock->expects($this->exactly(2)) - ->method('getOrderItemId') - ->willReturn($orderItemId); - - $this->creditmemoItemMock->expects($this->once()) - ->method('getProductId') - ->willReturn($productId); - - $this->orderItemRepositoryMock->expects($this->once()) - ->method('get') - ->with($orderItemId) - ->willReturn($this->orderItemMock); - - $this->orderMock->expects($this->once()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->storeManagerMock->expects($this->once()) - ->method('getStore') - ->with($storeId) - ->willReturn($this->storeMock); - - $this->storeMock->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($webSiteId); - - $this->stockManagementMock->expects($this->once()) - ->method('backItemQty') - ->with($productId, $qty, $webSiteId) - ->willReturn(true); - - $this->stockIndexerProcessorMock->expects($this->once()) - ->method('reindexList') - ->with([$productId]); - - $this->priceIndexerMock->expects($this->once()) - ->method('reindexList') - ->with([$productId]); - - $this->returnProcessor->execute($this->creditmemoMock, $this->orderMock, $returnToStockItems); - } -} +stockManagementMock = $this->getMockBuilder(StockManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockIndexerProcessorMock = $this->getMockBuilder( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class + )->disableOriginalConstructor() + ->getMock(); + $this->priceIndexerMock = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Price\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnProcessor = new ReturnProcessor( + $this->stockManagementMock, + $this->stockIndexerProcessorMock, + $this->priceIndexerMock, + $this->creditmemoRepositoryMock, + $this->storeManagerMock, + $this->orderRepositoryMock, + $this->orderItemRepositoryMock + ); + } + + public function testExecute() + { + $orderItemId = 99; + $productId = 50; + $returnToStockItems = [$orderItemId]; + $qty = 1; + $storeId = 0; + $webSiteId = 10; + + $this->creditmemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditmemoItemMock]); + + $this->creditmemoItemMock->expects($this->once()) + ->method('getQty') + ->willReturn($qty); + + $this->creditmemoItemMock->expects($this->exactly(2)) + ->method('getOrderItemId') + ->willReturn($orderItemId); + + $this->creditmemoItemMock->expects($this->once()) + ->method('getProductId') + ->willReturn($productId); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderItemId) + ->willReturn($this->orderItemMock); + + $this->orderMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with($storeId) + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->once()) + ->method('getWebsiteId') + ->willReturn($webSiteId); + + $this->stockManagementMock->expects($this->once()) + ->method('backItemQty') + ->with($productId, $qty, $webSiteId) + ->willReturn(true); + + $this->stockIndexerProcessorMock->expects($this->once()) + ->method('reindexList') + ->with([$productId]); + + $this->priceIndexerMock->expects($this->once()) + ->method('reindexList') + ->with([$productId]); + + $this->returnProcessor->execute($this->creditmemoMock, $this->orderMock, $returnToStockItems); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php index 9903881990cc8..da9d3fd8aefd1 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php @@ -1,134 +1,134 @@ -orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditMemoMock = $this->getMockBuilder(CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditMemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->returnValidator = new ReturnValidator( - $this->orderItemRepositoryMock - ); - } - - /** - * @dataProvider dataProvider - */ - public function testValidate( - $expectedResult, - $returnToStockItems, - $orderItemId, - $creditMemoItemId, - $productSku = null - ) { - $this->creditMemoMock->expects($this->once()) - ->method('getItems') - ->willReturn([$this->creditMemoItemMock]); - - $this->orderItemRepositoryMock->expects($this->once()) - ->method('get') - ->with($returnToStockItems[0]) - ->willReturn($this->orderItemMock); - - $this->orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - - $this->creditMemoItemMock->expects($this->once()) - ->method('getOrderItemId') - ->willReturn($creditMemoItemId); - - if ($productSku) { - $this->orderItemMock->expects($this->once()) - ->method('getSku') - ->willReturn($productSku); - } - - $this->assertEquals( - $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), - $expectedResult - ); - } - - public function testValidationWithWrongOrderItems() - { - $returnToStockItems = [1]; - $this->creditMemoMock->expects($this->once()) - ->method('getItems') - ->willReturn([$this->creditMemoItemMock]); - $this->orderItemRepositoryMock->expects($this->once()) - ->method('get') - ->with($returnToStockItems[0]) - ->willThrowException(new NoSuchEntityException); - - $this->assertEquals( - $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), - __('The return to stock argument contains product item that is not part of the original order.') - ); - - } - - public function dataProvider() - { - return [ - 'PostirivValidationTest' => [null, [1], 1, 1], - 'WithWrongReturnToStockItems' => [ - __('The "%1" product is not part of the current creditmemo.', 'sku1'), [2], 2, 1, 'sku1', - ], - ]; - } -} +orderItemRepositoryMock = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditMemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditMemoItemMock = $this->getMockBuilder(CreditmemoItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderItemMock = $this->getMockBuilder(OrderItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnValidator = new ReturnValidator( + $this->orderItemRepositoryMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testValidate( + $expectedResult, + $returnToStockItems, + $orderItemId, + $creditMemoItemId, + $productSku = null + ) { + $this->creditMemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditMemoItemMock]); + + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($returnToStockItems[0]) + ->willReturn($this->orderItemMock); + + $this->orderItemMock->expects($this->once()) + ->method('getItemId') + ->willReturn($orderItemId); + + $this->creditMemoItemMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn($creditMemoItemId); + + if ($productSku) { + $this->orderItemMock->expects($this->once()) + ->method('getSku') + ->willReturn($productSku); + } + + $this->assertEquals( + $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), + $expectedResult + ); + } + + public function testValidationWithWrongOrderItems() + { + $returnToStockItems = [1]; + $this->creditMemoMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->creditMemoItemMock]); + $this->orderItemRepositoryMock->expects($this->once()) + ->method('get') + ->with($returnToStockItems[0]) + ->willThrowException(new NoSuchEntityException); + + $this->assertEquals( + $this->returnValidator->validate($returnToStockItems, $this->creditMemoMock), + __('The return to stock argument contains product item that is not part of the original order.') + ); + + } + + public function dataProvider() + { + return [ + 'PostirivValidationTest' => [null, [1], 1, 1], + 'WithWrongReturnToStockItems' => [ + __('The "%1" product is not part of the current creditmemo.', 'sku1'), [2], 2, 1, 'sku1', + ], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php index 7f184b1081bf7..01a4f61933fd7 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockInvoiceTest.php @@ -1,188 +1,188 @@ -returnProcessorMock = $this->getMockBuilder(\Magento\SalesInventory\Model\Order\ReturnProcessor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\CreditmemoRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->invoiceRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\InvoiceRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->refundInvoiceMock = $this->getMockBuilder(\Magento\Sales\Api\RefundInvoiceInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoCreationArgumentsMock = $this->getMockBuilder( - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface::class - )->disableOriginalConstructor() - ->getMock(); - $this->extensionAttributesMock = $this->getMockBuilder( - \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface::class - )->disableOriginalConstructor() - ->setMethods(['getReturnToStockItems']) - ->getMock(); - $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->stockConfigurationMock = $this->getMockBuilder( - \Magento\CatalogInventory\Api\StockConfigurationInterface::class - )->disableOriginalConstructor() - ->getMock(); - - $this->returnToStock = new \Magento\SalesInventory\Model\Plugin\Order\ReturnToStockInvoice( - $this->returnProcessorMock, - $this->creditmemoRepositoryMock, - $this->orderRepositoryMock, - $this->invoiceRepositoryMock, - $this->stockConfigurationMock - ); - } - - public function testAroundExecute() - { - $orderId = 1; - $creditmemoId = 99; - $items = []; - $returnToStockItems = [1]; - $invoiceId = 98; - - $this->proceed = function () use ($creditmemoId) { - return $creditmemoId; - }; - $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributesMock); - - $this->invoiceRepositoryMock->expects($this->once()) - ->method('get') - ->with($invoiceId) - ->willReturn($this->invoiceMock); - - $this->extensionAttributesMock->expects($this->exactly(2)) - ->method('getReturnToStockItems') - ->willReturn($returnToStockItems); - - $this->orderRepositoryMock->expects($this->once()) - ->method('get') - ->with($orderId) - ->willReturn($this->orderMock); - - $this->creditmemoRepositoryMock->expects($this->once()) - ->method('get') - ->with($creditmemoId) - ->willReturn($this->creditmemoMock); - - $this->returnProcessorMock->expects($this->once()) - ->method('execute') - ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); - - $this->invoiceMock->expects($this->once()) - ->method('getOrderId') - ->willReturn($orderId); - - $this->stockConfigurationMock->expects($this->once()) - ->method('isAutoReturnEnabled') - ->willReturn(false); - - $this->assertEquals( - $this->returnToStock->aroundExecute( - $this->refundInvoiceMock, - $this->proceed, - $invoiceId, - $items, - false, - false, - false, - null, - $this->creditmemoCreationArgumentsMock - ), - $creditmemoId - ); - } -} +returnProcessorMock = $this->getMockBuilder(\Magento\SalesInventory\Model\Order\ReturnProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->refundInvoiceMock = $this->getMockBuilder(\Magento\Sales\Api\RefundInvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsInterface::class + )->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder( + \Magento\Sales\Api\Data\CreditmemoCreationArgumentsExtensionInterface::class + )->disableOriginalConstructor() + ->setMethods(['getReturnToStockItems']) + ->getMock(); + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder( + \Magento\CatalogInventory\Api\StockConfigurationInterface::class + )->disableOriginalConstructor() + ->getMock(); + + $this->returnToStock = new \Magento\SalesInventory\Model\Plugin\Order\ReturnToStockInvoice( + $this->returnProcessorMock, + $this->creditmemoRepositoryMock, + $this->orderRepositoryMock, + $this->invoiceRepositoryMock, + $this->stockConfigurationMock + ); + } + + public function testAroundExecute() + { + $orderId = 1; + $creditmemoId = 99; + $items = []; + $returnToStockItems = [1]; + $invoiceId = 98; + + $this->proceed = function () use ($creditmemoId) { + return $creditmemoId; + }; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->with($invoiceId) + ->willReturn($this->invoiceMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($this->orderMock); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + + $this->returnProcessorMock->expects($this->once()) + ->method('execute') + ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); + + $this->invoiceMock->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + + $this->stockConfigurationMock->expects($this->once()) + ->method('isAutoReturnEnabled') + ->willReturn(false); + + $this->assertEquals( + $this->returnToStock->aroundExecute( + $this->refundInvoiceMock, + $this->proceed, + $invoiceId, + $items, + false, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ), + $creditmemoId + ); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php index 19c2e04d06e1f..928c4b276cda9 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/ReturnToStockOrderTest.php @@ -1,166 +1,166 @@ -returnProcessorMock = $this->getMockBuilder(ReturnProcessor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->refundOrderMock = $this->getMockBuilder(RefundOrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getReturnToStockItems']) - ->getMock(); - $this->orderMock = $this->getMockBuilder(OrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->returnToStock = new ReturnToStockOrder( - $this->returnProcessorMock, - $this->creditmemoRepositoryMock, - $this->orderRepositoryMock, - $this->stockConfigurationMock - ); - } - - public function testAroundExecute() - { - $orderId = 1; - $creditmemoId = 99; - $items = []; - $returnToStockItems = [1]; - $this->proceed = function () use ($creditmemoId) { - return $creditmemoId; - }; - $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributesMock); - - $this->extensionAttributesMock->expects($this->exactly(2)) - ->method('getReturnToStockItems') - ->willReturn($returnToStockItems); - - $this->orderRepositoryMock->expects($this->once()) - ->method('get') - ->with($orderId) - ->willReturn($this->orderMock); - - $this->creditmemoRepositoryMock->expects($this->once()) - ->method('get') - ->with($creditmemoId) - ->willReturn($this->creditmemoMock); - - $this->returnProcessorMock->expects($this->once()) - ->method('execute') - ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); - - $this->stockConfigurationMock->expects($this->once()) - ->method('isAutoReturnEnabled') - ->willReturn(false); - - $this->assertEquals( - $this->returnToStock->aroundExecute( - $this->refundOrderMock, - $this->proceed, - $orderId, - $items, - false, - false, - null, - $this->creditmemoCreationArgumentsMock - ), - $creditmemoId - ); - } -} +returnProcessorMock = $this->getMockBuilder(ReturnProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->refundOrderMock = $this->getMockBuilder(RefundOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getReturnToStockItems']) + ->getMock(); + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->returnToStock = new ReturnToStockOrder( + $this->returnProcessorMock, + $this->creditmemoRepositoryMock, + $this->orderRepositoryMock, + $this->stockConfigurationMock + ); + } + + public function testAroundExecute() + { + $orderId = 1; + $creditmemoId = 99; + $items = []; + $returnToStockItems = [1]; + $this->proceed = function () use ($creditmemoId) { + return $creditmemoId; + }; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($this->orderMock); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + + $this->returnProcessorMock->expects($this->once()) + ->method('execute') + ->with($this->creditmemoMock, $this->orderMock, $returnToStockItems); + + $this->stockConfigurationMock->expects($this->once()) + ->method('isAutoReturnEnabled') + ->willReturn(false); + + $this->assertEquals( + $this->returnToStock->aroundExecute( + $this->refundOrderMock, + $this->proceed, + $orderId, + $items, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ), + $creditmemoId + ); + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php index a66268dee736b..ca24926974074 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php @@ -1,158 +1,158 @@ -returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) - ->setMethods(['getReturnToStockItems']) - ->disableOriginalConstructor() - ->getMock(); - - $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->refundInvoiceValidatorMock = $this->getMockBuilder(RefundInvoiceInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->orderMock = $this->getMockBuilder(OrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->proceed = function () { - return $this->validateResultMock; - }; - $this->plugin = new InvoiceRefundCreationArguments($this->returnValidatorMock); - } - - /** - * @dataProvider dataProvider - */ - public function testAroundValidation($errorMessage) - { - $returnToStockItems = [1]; - $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributesMock); - - $this->extensionAttributesMock->expects($this->exactly(2)) - ->method('getReturnToStockItems') - ->willReturn($returnToStockItems); - - $this->returnValidatorMock->expects($this->once()) - ->method('validate') - ->willReturn($errorMessage); - - $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) - ->method('addMessage') - ->with($errorMessage); - - $this->plugin->aroundValidate( - $this->refundInvoiceValidatorMock, - $this->proceed, - $this->invoiceMock, - $this->orderMock, - $this->creditmemoMock, - [], - false, - false, - false, - null, - $this->creditmemoCreationArgumentsMock - ); - } - - public function dataProvider() - { - return [ - 'withErrors' => ['Error!'], - 'withoutErrors' => ['null'], - ]; - } -} +returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->setMethods(['getReturnToStockItems']) + ->disableOriginalConstructor() + ->getMock(); + + $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->refundInvoiceValidatorMock = $this->getMockBuilder(RefundInvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->proceed = function () { + return $this->validateResultMock; + }; + $this->plugin = new InvoiceRefundCreationArguments($this->returnValidatorMock); + } + + /** + * @dataProvider dataProvider + */ + public function testAroundValidation($errorMessage) + { + $returnToStockItems = [1]; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->returnValidatorMock->expects($this->once()) + ->method('validate') + ->willReturn($errorMessage); + + $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) + ->method('addMessage') + ->with($errorMessage); + + $this->plugin->aroundValidate( + $this->refundInvoiceValidatorMock, + $this->proceed, + $this->invoiceMock, + $this->orderMock, + $this->creditmemoMock, + [], + false, + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'withErrors' => ['Error!'], + 'withoutErrors' => ['null'], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php index a3f3212f1f6eb..3fb29cd6ed121 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/OrderRefundCreationArgumentsTest.php @@ -1,151 +1,151 @@ -returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) - ->setMethods(['getReturnToStockItems']) - ->disableOriginalConstructor() - ->getMock(); - - $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->refundOrderValidatorMock = $this->getMockBuilder(RefundOrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->orderMock = $this->getMockBuilder(OrderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->proceed = function () { - return $this->validateResultMock; - }; - - $this->plugin = new OrderRefundCreationArguments($this->returnValidatorMock); - } - - /** - * @dataProvider dataProvider - */ - public function testAroundValidation($errorMessage) - { - $returnToStockItems = [1]; - $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributesMock); - - $this->extensionAttributesMock->expects($this->exactly(2)) - ->method('getReturnToStockItems') - ->willReturn($returnToStockItems); - - $this->returnValidatorMock->expects($this->once()) - ->method('validate') - ->willReturn($errorMessage); - - $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) - ->method('addMessage') - ->with($errorMessage); - - $this->plugin->aroundValidate( - $this->refundOrderValidatorMock, - $this->proceed, - $this->orderMock, - $this->creditmemoMock, - [], - false, - false, - null, - $this->creditmemoCreationArgumentsMock - ); - } - - /** - * @return array - */ - public function dataProvider() - { - return [ - 'withErrors' => ['Error!'], - 'withoutErrors' => ['null'], - ]; - } -} +returnValidatorMock = $this->getMockBuilder(ReturnValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoCreationArgumentsMock = $this->getMockBuilder(CreditmemoCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->extensionAttributesMock = $this->getMockBuilder(CreditmemoCreationArgumentsExtensionInterface::class) + ->setMethods(['getReturnToStockItems']) + ->disableOriginalConstructor() + ->getMock(); + + $this->validateResultMock = $this->getMockBuilder(ValidatorResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->refundOrderValidatorMock = $this->getMockBuilder(RefundOrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->proceed = function () { + return $this->validateResultMock; + }; + + $this->plugin = new OrderRefundCreationArguments($this->returnValidatorMock); + } + + /** + * @dataProvider dataProvider + */ + public function testAroundValidation($errorMessage) + { + $returnToStockItems = [1]; + $this->creditmemoCreationArgumentsMock->expects($this->exactly(3)) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + + $this->extensionAttributesMock->expects($this->exactly(2)) + ->method('getReturnToStockItems') + ->willReturn($returnToStockItems); + + $this->returnValidatorMock->expects($this->once()) + ->method('validate') + ->willReturn($errorMessage); + + $this->validateResultMock->expects($errorMessage ? $this->once() : $this->never()) + ->method('addMessage') + ->with($errorMessage); + + $this->plugin->aroundValidate( + $this->refundOrderValidatorMock, + $this->proceed, + $this->orderMock, + $this->creditmemoMock, + [], + false, + false, + null, + $this->creditmemoCreationArgumentsMock + ); + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + 'withErrors' => ['Error!'], + 'withoutErrors' => ['null'], + ]; + } +} diff --git a/app/code/Magento/SalesInventory/registration.php b/app/code/Magento/SalesInventory/registration.php index bd4ecdbd48803..edb96135508d2 100644 --- a/app/code/Magento/SalesInventory/registration.php +++ b/app/code/Magento/SalesInventory/registration.php @@ -1,11 +1,11 @@ -objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - } - - /** - * @dataProvider dataProvider - * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php - */ - public function testRefundWithReturnItemsToStock($qtyRefund) - { - $productSku = 'simple'; - /** @var \Magento\Sales\Model\Order $existingOrder */ - $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) - ->loadByIncrementId('100000001'); - $orderItems = $existingOrder->getItems(); - $orderItem = array_shift($orderItems); - $expectedItems = [['order_item_id' => $orderItem->getItemId(), 'qty' => $qtyRefund]]; - $qtyBeforeRefund = $this->getQtyInStockBySku($productSku); - - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => '/V1/order/' . $existingOrder->getEntityId() . '/refund', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_REFUND_ORDER_NAME, - 'serviceVersion' => 'V1', - 'operation' => self::SERVICE_REFUND_ORDER_NAME . 'execute', - ] - ]; - - $this->_webApiCall( - $serviceInfo, - [ - 'orderId' => $existingOrder->getEntityId(), - 'items' => $expectedItems, - 'arguments' => [ - 'extension_attributes' => [ - 'return_to_stock_items' => [ - (int)$orderItem->getItemId() - ], - ], - ], - ] - ); - - $qtyAfterRefund = $this->getQtyInStockBySku($productSku); - - try { - $this->assertEquals( - $qtyBeforeRefund + $expectedItems[0]['qty'], - $qtyAfterRefund, - 'Failed asserting qty of returned items incorrect.' - ); - - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - $this->fail('Failed asserting that Creditmemo was created'); - } - } - - /** - * @return array - */ - public function dataProvider() - { - return [ - 'refundAllOrderItems' => [2], - 'refundPartition' => [1], - ]; - } - - /** - * @param string $sku - * @return int - */ - private function getQtyInStockBySku($sku) - { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => '/V1/' . self::SERVICE_STOCK_ITEMS_NAME . "/$sku", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => 'catalogInventoryStockRegistryV1', - 'serviceVersion' => 'V1', - 'operation' => 'catalogInventoryStockRegistryV1GetStockItemBySku', - ], - ]; - $arguments = ['productSku' => $sku]; - $apiResult = $this->_webApiCall($serviceInfo, $arguments); - return $apiResult['qty']; - } -} +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @dataProvider dataProvider + * @magentoApiDataFixture Magento/Sales/_files/order_with_shipping_and_invoice.php + */ + public function testRefundWithReturnItemsToStock($qtyRefund) + { + $productSku = 'simple'; + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + $orderItems = $existingOrder->getItems(); + $orderItem = array_shift($orderItems); + $expectedItems = [['order_item_id' => $orderItem->getItemId(), 'qty' => $qtyRefund]]; + $qtyBeforeRefund = $this->getQtyInStockBySku($productSku); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getEntityId() . '/refund', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_REFUND_ORDER_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_REFUND_ORDER_NAME . 'execute', + ] + ]; + + $this->_webApiCall( + $serviceInfo, + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $expectedItems, + 'arguments' => [ + 'extension_attributes' => [ + 'return_to_stock_items' => [ + (int)$orderItem->getItemId() + ], + ], + ], + ] + ); + + $qtyAfterRefund = $this->getQtyInStockBySku($productSku); + + try { + $this->assertEquals( + $qtyBeforeRefund + $expectedItems[0]['qty'], + $qtyAfterRefund, + 'Failed asserting qty of returned items incorrect.' + ); + + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Creditmemo was created'); + } + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + 'refundAllOrderItems' => [2], + 'refundPartition' => [1], + ]; + } + + /** + * @param string $sku + * @return int + */ + private function getQtyInStockBySku($sku) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/' . self::SERVICE_STOCK_ITEMS_NAME . "/$sku", + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'catalogInventoryStockRegistryV1', + 'serviceVersion' => 'V1', + 'operation' => 'catalogInventoryStockRegistryV1GetStockItemBySku', + ], + ]; + $arguments = ['productSku' => $sku]; + $apiResult = $this->_webApiCall($serviceInfo, $arguments); + return $apiResult['qty']; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php index c57f53582870c..48cf991c848b0 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice_rollback.php @@ -1,6 +1,6 @@ - Date: Tue, 11 Oct 2016 15:19:46 +0300 Subject: [PATCH 23/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- .../Sales/Model/Order/Validation/RefundInvoice.php | 6 ++++-- .../Sales/Model/Order/Validation/RefundOrder.php | 2 +- .../Magento/Sales/Model/ValidatorResultMerger.php | 14 +++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php index 73bbcd43f26b9..35fbfd036ab53 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php @@ -116,8 +116,10 @@ public function validate( return $this->validatorResultMerger->merge( $orderValidationResult, $creditmemoValidationResult, - $invoiceValidationResult->getMessages(), - ...array_values($itemsValidation) + array_merge( + [$invoiceValidationResult->getMessages()], + $itemsValidation + ) ); } } diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php b/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php index 0e1e90c876e3e..f00af1586abf2 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php @@ -98,7 +98,7 @@ public function validate( return $this->validatorResultMerger->merge( $orderValidationResult, $creditmemoValidationResult, - ...array_values($itemsValidation) + $itemsValidation ); } } diff --git a/app/code/Magento/Sales/Model/ValidatorResultMerger.php b/app/code/Magento/Sales/Model/ValidatorResultMerger.php index 886c00066ff9b..2f17dfd149e5b 100644 --- a/app/code/Magento/Sales/Model/ValidatorResultMerger.php +++ b/app/code/Magento/Sales/Model/ValidatorResultMerger.php @@ -26,17 +26,25 @@ public function __construct(ValidatorResultInterfaceFactory $validatorResultInte } /** - * Merge two validator results and additional messages + * Merges two validator results and additional messages. * * @param ValidatorResultInterface $first * @param ValidatorResultInterface $second + * @param array $others + * * @return ValidatorResultInterface */ - public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second) + public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second, array $others = []) { - $messages = array_merge($first->getMessages(), $second->getMessages(), ...array_slice(func_get_args(), 2)); + $messages = array_merge($first->getMessages(), $second->getMessages()); + + foreach ($others as $messagesBunch) { + $messages = array_merge($messages, $messagesBunch); + } + /** @var ValidatorResultInterface $result */ $result = $this->validatorResultInterfaceFactory->create(); + foreach ($messages as $message) { $result->addMessage($message); } From 5c3fa895314caba01184c8dc53475c3085dafd16 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Tue, 11 Oct 2016 16:08:49 +0300 Subject: [PATCH 24/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- .../Model/Order/CreditmemoDocumentFactory.php | 2 +- .../Order/CreditmemoDocumentFactoryTest.php | 2 +- app/code/Magento/SalesInventory/composer.json | 12 +- composer.lock | 143 ++++++++++-------- 4 files changed, 84 insertions(+), 75 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php index 361e9ac4c91d4..26271135328c0 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php @@ -88,7 +88,7 @@ private function attachComment( ) { $commentData = [ 'comment' => $comment->getComment(), - 'is_visible_on_frontend' => $comment->getIsVisibleOnFront() + 'is_visible_on_front' => $comment->getIsVisibleOnFront() ]; $comment = $this->commentFactory->create(['data' => $commentData]); $comment->setParentId($creditmemo->getEntityId()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php index ab11290d37219..07b6e026b74e7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php @@ -158,7 +158,7 @@ private function commonFactoryFlow() [ 'data' => [ 'comment' => 'text', - 'is_visible_on_frontend' => null + 'is_visible_on_front' => null ] ] ) diff --git a/app/code/Magento/SalesInventory/composer.json b/app/code/Magento/SalesInventory/composer.json index 4008a220fd844..44e570caf336b 100644 --- a/app/code/Magento/SalesInventory/composer.json +++ b/app/code/Magento/SalesInventory/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-sales-inventory", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|~7.0.6", - "magento/module-catalog-inventory": "100.1.*", - "magento/module-sales": "100.1.*", - "magento/module-store": "100.1.*", - "magento/module-catalog": "101.0.*", - "magento/framework": "100.1.*" + "php": "~5.5.0|~5.6.0|~7.0.0", + "magento/module-catalog-inventory": "100.0.*", + "magento/module-sales": "100.0.*", + "magento/module-store": "100.0.*", + "magento/module-catalog": "100.0.*", + "magento/framework": "100.0.*" }, "type": "magento2-module", "version": "100.0.0", diff --git a/composer.lock b/composer.lock index ebe395b8bd461..cb4971983cc29 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "22776bfe7806fa08ecbfe25d5a726039", - "content-hash": "4b0f232bbe37d12c183970282c392c1c", + "hash": "b8b22934efe930c5d3176556829581ef", + "content-hash": "c6e52e66eefb57a637f8d8737f464024", "packages": [ { "name": "braintree/braintree_php", @@ -709,22 +709,30 @@ }, { "name": "psr/log", - "version": "1.0.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", "shasum": "" }, + "require": { + "php": ">=5.3.0" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "psr-0": { - "Psr\\Log\\": "" + "psr-4": { + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -738,25 +746,26 @@ } ], "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ "log", "psr", "psr-3" ], - "time": "2012-12-21 11:40:51" + "time": "2016-10-10 12:19:37" }, { "name": "seld/jsonlint", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "66834d3e3566bb5798db7294619388786ae99394" + "reference": "e827b5254d3e58c736ea2c5616710983d80b0b70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/66834d3e3566bb5798db7294619388786ae99394", - "reference": "66834d3e3566bb5798db7294619388786ae99394", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e827b5254d3e58c736ea2c5616710983d80b0b70", + "reference": "e827b5254d3e58c736ea2c5616710983d80b0b70", "shasum": "" }, "require": { @@ -789,7 +798,7 @@ "parser", "validator" ], - "time": "2015-11-21 02:21:41" + "time": "2016-09-14 15:17:56" }, { "name": "symfony/console", @@ -851,7 +860,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -911,16 +920,16 @@ }, { "name": "symfony/finder", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "60804d88691e4a73bbbb3035eb1d9f075c5c2c10" + "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/60804d88691e4a73bbbb3035eb1d9f075c5c2c10", - "reference": "60804d88691e4a73bbbb3035eb1d9f075c5c2c10", + "url": "https://api.github.com/repos/symfony/finder/zipball/bc24c8f5674c6f6841f2856b70e5d60784be5691", + "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691", "shasum": "" }, "require": { @@ -956,20 +965,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-07-26 08:02:44" + "time": "2016-09-28 00:10:16" }, { "name": "symfony/process", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c" + "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d20332e43e8774ff8870b394f3dd6020cc7f8e0c", - "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c", + "url": "https://api.github.com/repos/symfony/process/zipball/024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", + "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", "shasum": "" }, "require": { @@ -1005,7 +1014,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-07-28 11:13:19" + "time": "2016-09-29 14:03:54" }, { "name": "tedivm/jshrink", @@ -2654,35 +2663,35 @@ }, { "name": "fabpot/php-cs-fixer", - "version": "v1.11.6", + "version": "v1.12.2", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "41dc93abd2937a85a3889e28765231d574d2bac8" + "reference": "baa7112bef3b86c65fcfaae9a7a50436e3902b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/41dc93abd2937a85a3889e28765231d574d2bac8", - "reference": "41dc93abd2937a85a3889e28765231d574d2bac8", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/baa7112bef3b86c65fcfaae9a7a50436e3902b41", + "reference": "baa7112bef3b86c65fcfaae9a7a50436e3902b41", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.6", - "sebastian/diff": "~1.1", - "symfony/console": "~2.3|~3.0", - "symfony/event-dispatcher": "~2.1|~3.0", - "symfony/filesystem": "~2.1|~3.0", - "symfony/finder": "~2.1|~3.0", - "symfony/process": "~2.3|~3.0", - "symfony/stopwatch": "~2.5|~3.0" + "php": "^5.3.6 || >=7.0 <7.2", + "sebastian/diff": "^1.1", + "symfony/console": "^2.3 || ^3.0", + "symfony/event-dispatcher": "^2.1 || ^3.0", + "symfony/filesystem": "^2.1 || ^3.0", + "symfony/finder": "^2.1 || ^3.0", + "symfony/process": "^2.3 || ^3.0", + "symfony/stopwatch": "^2.5 || ^3.0" }, "conflict": { "hhvm": "<3.9" }, "require-dev": { "phpunit/phpunit": "^4.5|^5", - "satooshi/php-coveralls": "^0.7.1" + "satooshi/php-coveralls": "^1.0" }, "bin": [ "php-cs-fixer" @@ -2709,7 +2718,7 @@ ], "description": "A tool to automatically fix PHP code style", "abandoned": "friendsofphp/php-cs-fixer", - "time": "2016-07-22 06:46:28" + "time": "2016-09-27 07:57:59" }, { "name": "league/climate", @@ -3421,23 +3430,23 @@ }, { "name": "sebastian/environment", - "version": "1.3.7", + "version": "1.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", - "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^4.8 || ^5.0" }, "type": "library", "extra": { @@ -3467,7 +3476,7 @@ "environment", "hhvm" ], - "time": "2016-05-17 03:18:57" + "time": "2016-08-18 05:49:44" }, { "name": "sebastian/exporter", @@ -3754,16 +3763,16 @@ }, { "name": "symfony/config", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4275ef5b59f18959df0eee3991e9ca0cc208ffd4" + "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4275ef5b59f18959df0eee3991e9ca0cc208ffd4", - "reference": "4275ef5b59f18959df0eee3991e9ca0cc208ffd4", + "url": "https://api.github.com/repos/symfony/config/zipball/f8b1922bbda9d2ac86aecd649399040bce849fde", + "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde", "shasum": "" }, "require": { @@ -3803,20 +3812,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2016-07-26 08:02:44" + "time": "2016-09-14 20:31:12" }, { "name": "symfony/dependency-injection", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f2b5a00d176f6a201dc430375c0ef37706ea3d12" + "reference": "ee9ec9ac2b046462d341e9de7c4346142d335e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f2b5a00d176f6a201dc430375c0ef37706ea3d12", - "reference": "f2b5a00d176f6a201dc430375c0ef37706ea3d12", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/ee9ec9ac2b046462d341e9de7c4346142d335e75", + "reference": "ee9ec9ac2b046462d341e9de7c4346142d335e75", "shasum": "" }, "require": { @@ -3866,20 +3875,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2016-07-30 07:20:35" + "time": "2016-09-24 09:47:20" }, { "name": "symfony/filesystem", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ab4c3f085c8f5a56536845bf985c4cef30bf75fd" + "reference": "44b499521defddf2eae17a18c811bbdae4f98bdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ab4c3f085c8f5a56536845bf985c4cef30bf75fd", - "reference": "ab4c3f085c8f5a56536845bf985c4cef30bf75fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/44b499521defddf2eae17a18c811bbdae4f98bdf", + "reference": "44b499521defddf2eae17a18c811bbdae4f98bdf", "shasum": "" }, "require": { @@ -3915,11 +3924,11 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2016-07-20 05:41:28" + "time": "2016-09-06 10:55:00" }, { "name": "symfony/stopwatch", - "version": "v3.1.3", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -3968,16 +3977,16 @@ }, { "name": "symfony/yaml", - "version": "v2.8.9", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0ceab136f43ed9d3e97b3eea32a7855dc50c121d" + "reference": "e7540734bad981fe59f8ef14b6fc194ae9df8d9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0ceab136f43ed9d3e97b3eea32a7855dc50c121d", - "reference": "0ceab136f43ed9d3e97b3eea32a7855dc50c121d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e7540734bad981fe59f8ef14b6fc194ae9df8d9c", + "reference": "e7540734bad981fe59f8ef14b6fc194ae9df8d9c", "shasum": "" }, "require": { @@ -4013,7 +4022,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-07-17 09:06:15" + "time": "2016-09-02 01:57:56" } ], "aliases": [], From ce9809020ea0590aa241c8a973e49f78382c781b Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Tue, 18 Oct 2016 16:03:44 +0300 Subject: [PATCH 25/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Adminhtml/File/Address/Upload.php | 127 +++++++++++++ .../Adminhtml/File/Customer/Upload.php | 104 +++++++++++ .../Customer/Model/Customer/DataProvider.php | 110 ++++++++++- .../Magento/Customer/Model/FileProcessor.php | 144 ++++++++++++++- .../Magento/Customer/Model/FileUploader.php | 172 ++++++++++++++++++ .../Customer/Model/Metadata/Form/File.php | 111 ++++++++++- .../Customer/Model/Metadata/Form/Image.php | 88 +++++++++ .../Component/Form/Element/DataType/Media.php | 39 ---- .../Ui/view/base/web/js/form/element/media.js | 35 +--- .../web/templates/form/element/media.html | 58 ------ .../Magento/Framework/Api/ImageProcessor.php | 30 ++- 11 files changed, 870 insertions(+), 148 deletions(-) create mode 100644 app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php create mode 100644 app/code/Magento/Customer/Controller/Adminhtml/File/Customer/Upload.php create mode 100644 app/code/Magento/Customer/Model/FileUploader.php diff --git a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php new file mode 100644 index 0000000000000..d7f83ba6a6380 --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php @@ -0,0 +1,127 @@ +fileUploaderFactory = $fileUploaderFactory; + $this->addressMetadataService = $addressMetadataService; + $this->logger = $logger; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + public function execute() + { + try { + if (empty($_FILES)) { + throw new \Exception('$_FILES array is empty.'); + } + + // Must be executed before any operations with $_FILES! + $this->convertFilesArray(); + + $attributeCode = key($_FILES['address']['name']); + $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); + + /** @var FileUploader $fileUploader */ + $fileUploader = $this->fileUploaderFactory->create([ + 'attributeMetadata' => $attributeMetadata, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + 'scope' => 'address', + ]); + + $errors = $fileUploader->validate(); + if (true !== $errors) { + $errorMessage = implode('
', $errors); + throw new LocalizedException(__($errorMessage)); + } + + $result = $fileUploader->upload(); + } catch (LocalizedException $e) { + $result = [ + 'error' => $e->getMessage(), + 'errorcode' => $e->getCode(), + ]; + } catch (\Exception $e) { + $this->logger->critical($e); + $result = [ + 'error' => __('Something went wrong while saving file.'), + 'errorcode' => $e->getCode(), + ]; + } + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setData($result); + return $resultJson; + } + + /** + * Update global $_FILES array. Convert data to standard form + * + * NOTE: This conversion is required to use \Magento\Framework\File\Uploader::_setUploadFileId($fileId) method. + * + * @return void + */ + private function convertFilesArray() + { + foreach($_FILES['address'] as $itemKey => $item) { + foreach ($item as $value) { + if (is_array($value)) { + $_FILES['address'][$itemKey] = [ + key($value) => current($value), + ]; + } + } + } + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/File/Customer/Upload.php b/app/code/Magento/Customer/Controller/Adminhtml/File/Customer/Upload.php new file mode 100644 index 0000000000000..47bda9f30059c --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/File/Customer/Upload.php @@ -0,0 +1,104 @@ +fileUploaderFactory = $fileUploaderFactory; + $this->customerMetadataService = $customerMetadataService; + $this->logger = $logger; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + public function execute() + { + try { + if (empty($_FILES)) { + throw new \Exception('$_FILES array is empty.'); + } + + $attributeCode = key($_FILES['customer']['name']); + $attributeMetadata = $this->customerMetadataService->getAttributeMetadata($attributeCode); + + /** @var FileUploader $fileUploader */ + $fileUploader = $this->fileUploaderFactory->create([ + 'attributeMetadata' => $attributeMetadata, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'scope' => 'customer', + ]); + + $errors = $fileUploader->validate(); + if (true !== $errors) { + $errorMessage = implode('
', $errors); + throw new LocalizedException(__($errorMessage)); + } + + $result = $fileUploader->upload(); + } catch (LocalizedException $e) { + $result = [ + 'error' => $e->getMessage(), + 'errorcode' => $e->getCode(), + ]; + } catch (\Exception $e) { + $this->logger->critical($e); + $result = [ + 'error' => __('Something went wrong while saving file.'), + 'errorcode' => $e->getCode(), + ]; + } + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setData($result); + return $resultJson; + } +} diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index c52e6149983d1..f82ac01645865 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -5,14 +5,18 @@ */ namespace Magento\Customer\Model\Customer; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Model\Attribute; use Magento\Customer\Model\FileProcessor; use Magento\Customer\Model\FileProcessorFactory; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Type; use Magento\Customer\Model\Address; use Magento\Customer\Model\Customer; use Magento\Framework\App\ObjectManager; +use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; @@ -24,6 +28,11 @@ */ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider { + /** + * Maximum file size allowed for file_uploader UI component + */ + const MAX_FILE_SIZE = 2097152; + /** * @var Collection */ @@ -215,14 +224,21 @@ private function getFileUploaderData( if (!empty($file) && $fileProcessor->isExist($file) ) { + $stat = $fileProcessor->getStat($file); $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); } + $fileName = $file; + if (strrpos($fileName, '/') !== false) { + $fileName = substr($fileName, strrpos($fileName, '/') + 1); + } + if (!empty($file)) { return [ 'file' => $file, - 'type' => $attribute->getFrontendInput(), + 'size' => isset($stat) ? $stat['size'] : 0, 'url' => isset($viewUrl) ? $viewUrl : '', + 'name' => $fileName, ]; } return []; @@ -239,7 +255,7 @@ protected function getAttributesMeta(Type $entityType) { $meta = []; $attributes = $entityType->getAttributeCollection(); - /* @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ + /* @var AbstractAttribute $attribute */ foreach ($attributes as $attribute) { $code = $attribute->getAttributeCode(); // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value @@ -260,10 +276,100 @@ protected function getAttributesMeta(Type $entityType) if (!empty($rules)) { $meta[$code]['validation'] = $rules; } + + $this->overrideFileUploaderMetadata($entityType, $attribute, $meta[$code]); } return $meta; } + /** + * Override file uploader UI component metadata + * + * Overrides metadata for attributes with frontend_input equal to 'image' or 'file'. + * + * @param Type $entityType + * @param AbstractAttribute $attribute + * @param array $config + */ + private function overrideFileUploaderMetadata( + Type $entityType, + AbstractAttribute $attribute, + array &$config + ) { + if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { + $maxFileSize = self::MAX_FILE_SIZE; + + if (isset($config['validation']['max_file_size'])) { + $maxFileSize = (int)$config['validation']['max_file_size']; + } + + $allowedExtensions = []; + + if (isset($config['validation']['file_extensions'])) { + $allowedExtensions = explode(',', $config['validation']['file_extensions']); + array_walk($allowedExtensions, function(&$value) { $value = strtolower(trim($value)); }); + } + + $allowedExtensions = implode(' ', $allowedExtensions); + + $entityTypeCode = $entityType->getEntityTypeCode(); + $url = $this->getFileUploadUrl($entityTypeCode); + + $config = [ + 'formElement' => 'fileUploader', + 'componentType' => 'fileUploader', + 'maxFileSize' => $maxFileSize, + 'allowedExtensions' => $allowedExtensions, + 'uploaderConfig' => [ + 'url' => $url, + ], + 'label' => $this->getMetadataValue($config, 'label'), + 'sortOrder' => $this->getMetadataValue($config, 'sortOrder'), + 'required' => $this->getMetadataValue($config, 'required'), + 'visible' => $this->getMetadataValue($config, 'visible'), + 'validation' => $this->getMetadataValue($config, 'validation'), + ]; + } + } + + /** + * Retrieve metadata value + * + * @param array $config + * @param string $name + * @param mixed $default + * @return mixed + */ + private function getMetadataValue($config, $name, $default = null) + { + $value = isset($config[$name]) ? $config[$name] : $default; + return $value; + } + + /** + * Retrieve URL to file upload + * + * @param string $entityTypeCode + * @return string + */ + private function getFileUploadUrl($entityTypeCode) + { + switch ($entityTypeCode) { + case CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER: + $url = 'customer/file/customer_upload'; + break; + + case AddressMetadataInterface::ENTITY_TYPE_ADDRESS: + $url = 'customer/file/address_upload'; + break; + + default: + $url = ''; + break; + } + return $url; + } + /** * Prepare address data * diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index f2aac7a295290..c98a85cc18463 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -8,17 +8,31 @@ use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; class FileProcessor { + /** + * Temporary directory name + */ + const TMP_DIR = 'tmp'; + /** * @var WriteInterface */ private $mediaDirectory; + /** + * @var UploaderFactory + */ + private $uploaderFactory; + /** * @var UrlInterface */ @@ -34,22 +48,63 @@ class FileProcessor */ private $entityTypeCode; + /** + * @var array + */ + private $allowedExtensions = []; + /** * @param Filesystem $filesystem + * @param UploaderFactory $uploaderFactory * @param UrlInterface $urlBuilder * @param EncoderInterface $urlEncoder * @param string $entityTypeCode + * @param array $allowedExtensions */ public function __construct( Filesystem $filesystem, + UploaderFactory $uploaderFactory, UrlInterface $urlBuilder, EncoderInterface $urlEncoder, - $entityTypeCode + $entityTypeCode, + array $allowedExtensions = [] ) { $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->uploaderFactory = $uploaderFactory; $this->urlBuilder = $urlBuilder; $this->urlEncoder = $urlEncoder; $this->entityTypeCode = $entityTypeCode; + $this->allowedExtensions = $allowedExtensions; + } + + /** + * Retrieve base64 encoded file content + * + * @param string $fileName + * @return string + */ + public function getBase64EncodedData($fileName) + { + $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + + $fileContent = $this->mediaDirectory->readFile($filePath); + + $encodedContent = base64_encode($fileContent); + return $encodedContent; + } + + /** + * Get file statistics data + * + * @param string $fileName + * @return array + */ + public function getStat($fileName) + { + $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + + $result = $this->mediaDirectory->stat($filePath); + return $result; } /** @@ -92,4 +147,91 @@ public function getViewUrl($filePath, $type) return $viewUrl; } + + /** + * Save uploaded file to temporary directory + * + * @param string $fileId + * @return \string[] + * @throws LocalizedException + */ + public function saveTemporaryFile($fileId) + { + /** @var Uploader $uploader */ + $uploader = $this->uploaderFactory->create(['fileId' => $fileId]); + $uploader->setFilesDispersion(false); + $uploader->setFilenamesCaseSensitivity(false); + $uploader->setAllowRenameFiles(true); + $uploader->setAllowedExtensions($this->allowedExtensions); + + $path = $this->mediaDirectory->getAbsolutePath( + $this->entityTypeCode . '/' . self::TMP_DIR + ); + + $result = $uploader->save($path); + if (!$result) { + throw new LocalizedException(__('File can not be saved to the destination folder.')); + } + + return $result; + } + + /** + * Move file from temporary directory into base directory + * + * @param string $fileName + * @return string + * @throws LocalizedException + */ + public function moveTemporaryFile($fileName) + { + $fileName = ltrim($fileName, '/'); + + $dispersionPath = Uploader::getDispretionPath($fileName); + $destinationPath = $this->entityTypeCode . $dispersionPath; + + if (!$this->mediaDirectory->create($destinationPath)) { + throw new LocalizedException( + __('Unable to create directory %1.', $destinationPath) + ); + } + + if (!$this->mediaDirectory->isWritable($destinationPath)) { + throw new LocalizedException( + __('Destination folder is not writable or does not exists.') + ); + } + + $destinationFileName = Uploader::getNewFileName( + $this->mediaDirectory->getAbsolutePath($destinationPath) . '/' . $fileName + ); + + try { + $this->mediaDirectory->renameFile( + $this->entityTypeCode . '/' . self::TMP_DIR . '/' . $fileName, + $destinationPath . '/' . $destinationFileName + ); + } catch (\Exception $e) { + throw new LocalizedException( + __('Something went wrong while saving the file.') + ); + } + + $fileName = $dispersionPath . '/' . $fileName; + return $fileName; + } + + /** + * Remove uploaded file + * + * @param string $fileName + * @return bool + */ + public function removeUploadedFile($fileName) + { + $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + + $result = $this->mediaDirectory->delete($filePath); + return $result; + } } diff --git a/app/code/Magento/Customer/Model/FileUploader.php b/app/code/Magento/Customer/Model/FileUploader.php new file mode 100644 index 0000000000000..97dfdafdf7575 --- /dev/null +++ b/app/code/Magento/Customer/Model/FileUploader.php @@ -0,0 +1,172 @@ +customerMetadataService = $customerMetadataService; + $this->addressMetadataService = $addressMetadataService; + $this->elementFactory = $elementFactory; + $this->fileProcessorFactory = $fileProcessorFactory; + $this->attributeMetadata = $attributeMetadata; + $this->entityTypeCode = $entityTypeCode; + $this->scope = $scope; + } + + /** + * Validate uploaded file + * + * @return array|bool + */ + public function validate() + { + $formElement = $this->elementFactory->create( + $this->attributeMetadata, + null, + $this->entityTypeCode + ); + + $errors = $formElement->validateValue($this->getData()); + return $errors; + } + + /** + * Execute file uploading + * + * @return \string[] + * @throws LocalizedException + */ + public function upload() + { + /** @var FileProcessor $fileProcessor */ + $fileProcessor = $this->fileProcessorFactory->create([ + 'entityTypeCode' => $this->entityTypeCode, + 'allowedExtensions' => $this->getAllowedExtensions(), + ]); + + $result = $fileProcessor->saveTemporaryFile($this->scope . '[' . $this->getAttributeCode() . ']'); + + // Update tmp_name param. Required for attribute validation! + $result['tmp_name'] = $result['path'] . '/' . ltrim($result['file'], '/'); + + $result['url'] = $fileProcessor->getViewUrl( + FileProcessor::TMP_DIR . '/' . ltrim($result['name'], '/'), + $this->attributeMetadata->getFrontendInput() + ); + + return $result; + } + + /** + * Get attribute code + * + * @return string + */ + private function getAttributeCode() + { + return key($_FILES[$this->scope]['name']); + } + + /** + * Retrieve data from global $_FILES array + * + * @return array + */ + private function getData() + { + $data = []; + + $fileAttributes = $_FILES[$this->scope]; + foreach ($fileAttributes as $attributeName => $attributeValue) { + $data[$attributeName] = $attributeValue[$this->getAttributeCode()]; + } + + return $data; + } + + /** + * Get allowed extensions + * + * @return array + */ + private function getAllowedExtensions() + { + $allowedExtensions = []; + + $validationRules = $this->attributeMetadata->getValidationRules(); + foreach ($validationRules as $validationRule) { + if ($validationRule->getName() == 'file_extensions') { + $allowedExtensions = explode(',', $validationRule->getValue()); + array_walk($allowedExtensions, function (&$value) { + $value = strtolower(trim($value)); + }); + break; + } + } + + return $allowedExtensions; + } +} diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index b94228cfa67b5..0ecb966328a4e 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -7,6 +7,10 @@ */ namespace Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Model\FileProcessor; +use Magento\Customer\Model\FileProcessorFactory; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; use Magento\Framework\App\Filesystem\DirectoryList; @@ -46,6 +50,16 @@ class File extends AbstractData */ private $uploaderFactory; + /** + * @var FileProcessor + */ + protected $fileProcessor; + + /** + * @var FileProcessorFactory + */ + protected $fileProcessorFactory; + /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Psr\Log\LoggerInterface $logger @@ -86,10 +100,6 @@ public function __construct( */ public function extractValue(\Magento\Framework\App\RequestInterface $request) { - if ($this->getIsAjaxRequest()) { - return false; - } - $extend = $this->_getRequestValue($request); $attrCode = $this->getAttribute()->getAttributeCode(); @@ -117,6 +127,14 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) $value[$fileKey] = $scopeData[$attrCode]; } } + } else if (isset($extend[0]['file']) && !empty($extend[0]['file'])) { + /** + * This case is required by file uploader UI component + * + * $extend[0]['file'] - uses for AJAX validation + * $extend[0] - uses for POST request + */ + $value = $this->getIsAjaxRequest() ? $extend[0]['file'] : $extend[0]; } else { $value = []; } @@ -196,7 +214,17 @@ protected function _validateByRules($value) */ protected function _isUploadedFile($filename) { - return is_uploaded_file($filename); + if (is_uploaded_file($filename)) { + return true; + } + + // This case is required for file uploader UI component + $temporaryFile = FileProcessor::TMP_DIR . '/' . pathinfo($filename)['basename']; + if ($this->getFileProcessor()->isExist($temporaryFile)) { + return true; + } + + return false; } /** @@ -243,7 +271,7 @@ public function validateValue($value) /** * {@inheritdoc} * - * @return $this|string + * @return ImageContentInterface|array|string|null */ public function compactValue($value) { @@ -251,6 +279,44 @@ public function compactValue($value) return $this; } + // Remove outdated file (in the case of file uploader UI component) + if (empty($value) && !empty($this->_value)) { + $this->getFileProcessor()->removeUploadedFile($this->_value); + return $value; + } + + if (isset($value['file']) && !empty($value['file'])) { + if ($value['file'] == $this->_value) { + return $this->_value; + } + $result = $this->processUiComponentValue($value); + } else { + $result = $this->processInputFieldValue($value); + } + + return $result; + } + + /** + * Process file uploader UI component data + * + * @param array $value + * @return string|null + */ + protected function processUiComponentValue(array $value) + { + $result = $this->getFileProcessor()->moveTemporaryFile($value['file']); + return $result; + } + + /** + * Process input type=file component data + * + * @param string $value + * @return bool|int|string + */ + protected function processInputFieldValue($value) + { $toDelete = false; if ($this->_value) { if (!$this->getAttribute()->isRequired() @@ -267,8 +333,8 @@ public function compactValue($value) $result = $this->_value; if ($toDelete) { - $result = ''; $mediaDir->delete($this->_entityTypeCode . '/' . ltrim($this->_value, '/')); + $result = ''; } if (!empty($value['tmp_name'])) { @@ -311,4 +377,35 @@ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFa return $output; } + + /** + * Get FileProcessor instance + * + * @return FileProcessor + */ + protected function getFileProcessor() + { + if ($this->fileProcessor === null) { + $this->fileProcessor = $this->getFileProcessorFactory()->create([ + 'entityTypeCode' => $this->_entityTypeCode, + ]); + } + return $this->fileProcessor; + } + + /** + * Get FileProcessorFactory instance + * + * @return FileProcessorFactory + * + * @deprecated + */ + protected function getFileProcessorFactory() + { + if ($this->fileProcessorFactory === null) { + $this->fileProcessorFactory = ObjectManager::getInstance() + ->get(FileProcessorFactory::class); + } + return $this->fileProcessorFactory; + } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index 003077d6c4c5f..ca732ad2e559f 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -7,10 +7,21 @@ */ namespace Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\FileProcessor; use Magento\Framework\Api\ArrayObjectSearch; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\ObjectManager; class Image extends File { + /** + * @var ImageContentInterfaceFactory + */ + private $imageContentFactory; + /** * Validate file by attribute validate rules * Return array of errors @@ -79,4 +90,81 @@ protected function _validateByRules($value) return $errors; } + + /** + * Process file uploader UI component data + * + * @param array $value + * @return bool|int|ImageContentInterface|string + */ + protected function processUiComponentValue(array $value) + { + if ($this->_entityTypeCode == AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { + $result = $this->processCustomerAddressValue($value); + return $result; + } + + if ($this->_entityTypeCode == CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { + $result = $this->processCustomerValue($value); + return $result; + } + + return $this->_value; + } + + /** + * Process file uploader UI component data for customer_address entity + * + * @param array $value + * @return string + */ + protected function processCustomerAddressValue(array $value) + { + $result = $this->getFileProcessor()->moveTemporaryFile($value['file']); + return $result; + } + + /** + * Process file uploader UI component data for customer entity + * + * @param array $value + * @return bool|int|ImageContentInterface|string + */ + protected function processCustomerValue(array $value) + { + $temporaryFile = FileProcessor::TMP_DIR . '/' . ltrim($value['file'], '/'); + + if ($this->getFileProcessor()->isExist($temporaryFile)) { + $base64EncodedData = $this->getFileProcessor()->getBase64EncodedData($temporaryFile); + + /** @var ImageContentInterface $imageContentDataObject */ + $imageContentDataObject = $this->getImageContentFactory()->create() + ->setName($value['name']) + ->setBase64EncodedData($base64EncodedData) + ->setType($value['type']); + + // Remove temporary file + $this->getFileProcessor()->removeUploadedFile($temporaryFile); + + return $imageContentDataObject; + } + + return $this->_value; + } + + /** + * Get ImageContentInterfaceFactory instance + * + * @return ImageContentInterfaceFactory + * + * @deprecated + */ + private function getImageContentFactory() + { + if ($this->imageContentFactory === null) { + $this->imageContentFactory = ObjectManager::getInstance() + ->get(ImageContentInterfaceFactory::class); + } + return $this->imageContentFactory; + } } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php index 500e453f6d96b..264b948f34b89 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php @@ -5,9 +5,6 @@ */ namespace Magento\Ui\Component\Form\Element\DataType; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\View\Asset\Repository; - /** * Class Media */ @@ -15,11 +12,6 @@ class Media extends AbstractDataType { const NAME = 'media'; - /** - * @var Repository - */ - private $assetRepo; - /** * Get component name * @@ -29,35 +21,4 @@ public function getComponentName() { return static::NAME; } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $config = $this->getData('config'); - - $config['placeholder'] = $this->getAssetRepo()->getUrl('images/fam_bullet_disk.gif'); - - $this->setData('config', $config); - - parent::prepare(); - } - - /** - * Get Repository instance - * - * @return Repository - * - * @deprecated - */ - private function getAssetRepo() - { - if ($this->assetRepo === null) { - $this->assetRepo = ObjectManager::getInstance()->get(Repository::class); - } - return $this->assetRepo; - } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/media.js b/app/code/Magento/Ui/view/base/web/js/form/element/media.js index 449b6b571b631..f8c27b55120cc 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/media.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/media.js @@ -10,16 +10,9 @@ define([ return Abstract.extend({ defaults: { - deleteCheckbox: false, links: { - value: '', - file: '${ $.provider }:${ $.dataScope }.file', - type: '${ $.provider }:${ $.dataScope }.type', - url: '${ $.provider }:${ $.dataScope }.url', - deleteCheckbox: '${ $.provider }:${ $.dataScope }.delete' - }, - width: 22, - height: 22 + value: '' + } }, /** @@ -34,19 +27,6 @@ define([ return this; }, - /** - * Initializes observable properties of instance - * - * @returns {Abstract} Chainable. - */ - initObservable: function () { - this._super(); - - this.observe('deleteCheckbox'); - - return this; - }, - /** * Defines form ID with which file input will be associated. * @@ -63,17 +43,6 @@ define([ this.formId = namespace[0]; return this; - }, - - /** - * Calls global image preview handler - */ - callPreviewHandler: function () { - - /* eslint-disable no-undef */ - imagePreview('image-' + this.uid); - - /* eslint-enable no-undef */ } }); }); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/media.html b/app/code/Magento/Ui/view/base/web/templates/form/element/media.html index e0198ae77f45a..61cab3bc34961 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/media.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/media.html @@ -5,38 +5,6 @@ */ --> - - - - - - - -
- - - - -
- - - - - - - - - - - - - - diff --git a/lib/internal/Magento/Framework/Api/ImageProcessor.php b/lib/internal/Magento/Framework/Api/ImageProcessor.php index 7436f412ddcd6..c3f479620233b 100644 --- a/lib/internal/Magento/Framework/Api/ImageProcessor.php +++ b/lib/internal/Magento/Framework/Api/ImageProcessor.php @@ -121,7 +121,7 @@ public function save( ); if ($previousImageAttribute) { $previousImagePath = $previousImageAttribute->getValue(); - if (!empty($previousImagePath)) { + if (!empty($previousImagePath) && ($previousImagePath != $filename)) { @unlink($this->mediaDirectory->getAbsolutePath() . $entityType . $previousImagePath); } } @@ -142,11 +142,12 @@ public function processImageContent($entityType, $imageContent) $fileContent = @base64_decode($imageContent->getBase64EncodedData(), true); $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); - $fileName = substr(md5(rand()), 0, 7) . '.' . $imageContent->getName(); - $tmpDirectory->writeFile($fileName, $fileContent); + $fileName = $this->getFileName($imageContent); + $tmpFileName = substr(md5(rand()), 0, 7) . '.' . $fileName; + $tmpDirectory->writeFile($tmpFileName, $fileContent); $fileAttributes = [ - 'tmp_name' => $tmpDirectory->getAbsolutePath() . $fileName, + 'tmp_name' => $tmpDirectory->getAbsolutePath() . $tmpFileName, 'name' => $imageContent->getName() ]; @@ -169,10 +170,23 @@ public function processImageContent($entityType, $imageContent) */ protected function getMimeTypeExtension($mimeType) { - if (isset($this->mimeTypeExtensionMap[$mimeType])) { - return $this->mimeTypeExtensionMap[$mimeType]; - } else { - return ""; + return isset($this->mimeTypeExtensionMap[$mimeType]) ? $this->mimeTypeExtensionMap[$mimeType] : ''; + } + + /** + * @param ImageContentInterface $imageContent + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getFileName($imageContent) + { + $fileName = $imageContent->getName(); + if (!pathinfo($fileName, PATHINFO_EXTENSION)) { + if (!$imageContent->getType() || !$this->getMimeTypeExtension($imageContent->getType())) { + throw new InputException(new Phrase('Cannot recognize image extension.')); + } + $fileName .= '.' . $this->getMimeTypeExtension($imageContent->getType()); } + return $fileName; } } From a8bc0fd56e8c2ede39bd752d53f63207989f33f7 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Tue, 18 Oct 2016 18:55:56 +0300 Subject: [PATCH 26/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- .../Sales/Model/Order/Validation/RefundInvoice.php | 10 +++++----- .../Sales/Model/Order/Validation/RefundOrder.php | 4 +++- app/code/Magento/Sales/Model/ValidatorResultMerger.php | 5 ++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php index 35fbfd036ab53..a0c32430d0c81 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php @@ -99,11 +99,13 @@ public function validate( $itemsValidation = []; foreach ($items as $item) { - $itemsValidation[] = $this->itemCreationValidator->validate( + $itemValidation = $this->itemCreationValidator->validate( $item, [CreationQuantityValidator::class], $order )->getMessages(); + + $itemsValidation = array_merge($itemsValidation, $itemValidation); } $invoiceValidationResult = $this->invoiceValidator->validate( @@ -116,10 +118,8 @@ public function validate( return $this->validatorResultMerger->merge( $orderValidationResult, $creditmemoValidationResult, - array_merge( - [$invoiceValidationResult->getMessages()], - $itemsValidation - ) + $invoiceValidationResult->getMessages(), + $itemsValidation ); } } diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php b/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php index f00af1586abf2..f1f362c797235 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundOrder.php @@ -88,11 +88,13 @@ public function validate( $itemsValidation = []; foreach ($items as $item) { - $itemsValidation[] = $this->itemCreationValidator->validate( + $itemValidation = $this->itemCreationValidator->validate( $item, [CreationQuantityValidator::class], $order )->getMessages(); + + $itemsValidation = array_merge($itemsValidation, $itemValidation); } return $this->validatorResultMerger->merge( diff --git a/app/code/Magento/Sales/Model/ValidatorResultMerger.php b/app/code/Magento/Sales/Model/ValidatorResultMerger.php index 2f17dfd149e5b..0f0b46284a8e6 100644 --- a/app/code/Magento/Sales/Model/ValidatorResultMerger.php +++ b/app/code/Magento/Sales/Model/ValidatorResultMerger.php @@ -30,15 +30,14 @@ public function __construct(ValidatorResultInterfaceFactory $validatorResultInte * * @param ValidatorResultInterface $first * @param ValidatorResultInterface $second - * @param array $others * * @return ValidatorResultInterface */ - public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second, array $others = []) + public function merge(ValidatorResultInterface $first, ValidatorResultInterface $second) { $messages = array_merge($first->getMessages(), $second->getMessages()); - foreach ($others as $messagesBunch) { + foreach (array_slice(func_get_args(), 2) as $messagesBunch) { $messages = array_merge($messages, $messagesBunch); } From 03ec4a2e4f6a76c11702266b62f226a174e911f9 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Wed, 19 Oct 2016 16:55:06 +0300 Subject: [PATCH 27/48] MAGETWO-59424: [BackPort] Ability to return the product to the stock after Creditmemo API - for 2.0.11 --- app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php index a0c32430d0c81..1a42c0e6bd095 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Validation/RefundInvoice.php @@ -19,6 +19,8 @@ /** * Class RefundInvoice + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RefundInvoice implements RefundInvoiceInterface { From 71aca4f8fb243daf355de08bba1ea7bad4cdc034 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 19 Oct 2016 17:58:59 +0300 Subject: [PATCH 28/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Model/Customer/DataProvider.php | 10 +- .../Adminhtml/File/Address/UploadTest.php | 234 +++++++++++++++ .../Adminhtml/File/Customer/UploadTest.php | 230 +++++++++++++++ .../Unit/Model/Customer/DataProviderTest.php | 173 ++++++++++- .../Test/Unit/Model/FileProcessorTest.php | 275 +++++++++++++++++- .../Test/Unit/Model/FileUploaderTest.php | 186 ++++++++++++ .../Unit/Model/Metadata/Form/FileTest.php | 84 +++++- .../Unit/Model/Metadata/Form/ImageTest.php | 194 +++++++++--- .../Component/Form/Element/DataType/Media.php | 24 ++ .../Ui/DataProvider/EavValidationRules.php | 39 ++- .../Magento/Framework/Api/ImageProcessor.php | 3 +- .../Api/Test/Unit/Api/ImageProcessorTest.php | 6 + 12 files changed, 1374 insertions(+), 84 deletions(-) create mode 100644 app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php create mode 100644 app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Customer/UploadTest.php create mode 100644 app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index f82ac01645865..87805506e2262 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -235,10 +235,12 @@ private function getFileUploaderData( if (!empty($file)) { return [ - 'file' => $file, - 'size' => isset($stat) ? $stat['size'] : 0, - 'url' => isset($viewUrl) ? $viewUrl : '', - 'name' => $fileName, + [ + 'file' => $file, + 'size' => isset($stat) ? $stat['size'] : 0, + 'url' => isset($viewUrl) ? $viewUrl : '', + 'name' => $fileName, + ], ]; } return []; diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php new file mode 100644 index 0000000000000..2853a39982367 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php @@ -0,0 +1,234 @@ +resultFactory = $this->getMockBuilder('Magento\Framework\Controller\ResultFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->context = $this->getMockBuilder('Magento\Backend\App\Action\Context') + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->once()) + ->method('getResultFactory') + ->willReturn($this->resultFactory); + + $this->fileUploaderFactory = $this->getMockBuilder('Magento\Customer\Model\FileUploaderFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->addressMetadataService = $this->getMockBuilder('Magento\Customer\Api\AddressMetadataInterface') + ->getMockForAbstractClass(); + + $this->logger = $this->getMockBuilder('Psr\Log\LoggerInterface') + ->getMockForAbstractClass(); + + $this->controller = new Upload( + $this->context, + $this->fileUploaderFactory, + $this->addressMetadataService, + $this->logger + ); + } + + public function testExecuteEmptyFiles() + { + $exception = new \Exception('$_FILES array is empty.'); + $this->logger->expects($this->once()) + ->method('critical') + ->with($exception) + ->willReturnSelf(); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with([ + 'error' => __('Something went wrong while saving file.'), + 'errorcode' => 0, + ]) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } + + public function testExecute() + { + $attributeCode = 'attribute_code'; + + $_FILES = [ + 'address' => [ + 'name' => [ + 'new_0' => [ + $attributeCode => 'filename', + ], + ], + ], + ]; + + $resultFileName = '/filename.ext1'; + $resultFilePath = 'filepath'; + $resultFileUrl = 'viewFileUrl'; + + $result = [ + 'name' => $resultFileName, + 'file' => $resultFileName, + 'path' => $resultFilePath, + 'tmp_name' => $resultFilePath . $resultFileName, + 'url' => $resultFileUrl, + ]; + + $attributeMetadataMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') + ->getMockForAbstractClass(); + + $this->addressMetadataService->expects($this->once()) + ->method('getAttributeMetadata') + ->with($attributeCode) + ->willReturn($attributeMetadataMock); + + $fileUploader = $this->getMockBuilder('Magento\Customer\Model\FileUploader') + ->disableOriginalConstructor() + ->getMock(); + $fileUploader->expects($this->once()) + ->method('validate') + ->willReturn(true); + $fileUploader->expects($this->once()) + ->method('upload') + ->willReturn($result); + + $this->fileUploaderFactory->expects($this->once()) + ->method('create') + ->with([ + 'attributeMetadata' => $attributeMetadataMock, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + 'scope' => 'address', + ]) + ->willReturn($fileUploader); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with($result) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } + + public function testExecuteWithErrors() + { + $attributeCode = 'attribute_code'; + + $_FILES = [ + 'address' => [ + 'name' => [ + 'new_0' => [ + $attributeCode => 'filename', + ], + ], + ], + ]; + + $errors = [ + 'error1', + 'error2', + ]; + + $attributeMetadataMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') + ->getMockForAbstractClass(); + + $this->addressMetadataService->expects($this->once()) + ->method('getAttributeMetadata') + ->with($attributeCode) + ->willReturn($attributeMetadataMock); + + $fileUploader = $this->getMockBuilder('Magento\Customer\Model\FileUploader') + ->disableOriginalConstructor() + ->getMock(); + $fileUploader->expects($this->once()) + ->method('validate') + ->willReturn($errors); + + $this->fileUploaderFactory->expects($this->once()) + ->method('create') + ->with([ + 'attributeMetadata' => $attributeMetadataMock, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + 'scope' => 'address', + ]) + ->willReturn($fileUploader); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with([ + 'error' => implode('
', $errors), + 'errorcode' => 0, + ]) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Customer/UploadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Customer/UploadTest.php new file mode 100644 index 0000000000000..29ac801bb5885 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Customer/UploadTest.php @@ -0,0 +1,230 @@ +resultFactory = $this->getMockBuilder('Magento\Framework\Controller\ResultFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->context = $this->getMockBuilder('Magento\Backend\App\Action\Context') + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->once()) + ->method('getResultFactory') + ->willReturn($this->resultFactory); + + $this->fileUploaderFactory = $this->getMockBuilder('Magento\Customer\Model\FileUploaderFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->customerMetadataService = $this->getMockBuilder('Magento\Customer\Api\CustomerMetadataInterface') + ->getMockForAbstractClass(); + + $this->logger = $this->getMockBuilder('Psr\Log\LoggerInterface') + ->getMockForAbstractClass(); + + $this->controller = new Upload( + $this->context, + $this->fileUploaderFactory, + $this->customerMetadataService, + $this->logger + ); + } + + public function testExecuteEmptyFiles() + { + $exception = new \Exception('$_FILES array is empty.'); + $this->logger->expects($this->once()) + ->method('critical') + ->with($exception) + ->willReturnSelf(); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with([ + 'error' => __('Something went wrong while saving file.'), + 'errorcode' => 0, + ]) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } + + public function testExecute() + { + $attributeCode = 'attribute_code'; + + $_FILES = [ + 'customer' => [ + 'name' => [ + $attributeCode => 'filename', + ], + ], + ]; + + $resultFileName = '/filename.ext1'; + $resultFilePath = 'filepath'; + $resultFileUrl = 'viewFileUrl'; + + $result = [ + 'name' => $resultFileName, + 'file' => $resultFileName, + 'path' => $resultFilePath, + 'tmp_name' => $resultFilePath . $resultFileName, + 'url' => $resultFileUrl, + ]; + + $attributeMetadataMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') + ->getMockForAbstractClass(); + + $this->customerMetadataService->expects($this->once()) + ->method('getAttributeMetadata') + ->with($attributeCode) + ->willReturn($attributeMetadataMock); + + $fileUploader = $this->getMockBuilder('Magento\Customer\Model\FileUploader') + ->disableOriginalConstructor() + ->getMock(); + $fileUploader->expects($this->once()) + ->method('validate') + ->willReturn(true); + $fileUploader->expects($this->once()) + ->method('upload') + ->willReturn($result); + + $this->fileUploaderFactory->expects($this->once()) + ->method('create') + ->with([ + 'attributeMetadata' => $attributeMetadataMock, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'scope' => 'customer', + ]) + ->willReturn($fileUploader); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with($result) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } + + public function testExecuteWithErrors() + { + $attributeCode = 'attribute_code'; + + $_FILES = [ + 'customer' => [ + 'name' => [ + $attributeCode => 'filename', + ], + ], + ]; + + $errors = [ + 'error1', + 'error2', + ]; + + $attributeMetadataMock = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') + ->getMockForAbstractClass(); + + $this->customerMetadataService->expects($this->once()) + ->method('getAttributeMetadata') + ->with($attributeCode) + ->willReturn($attributeMetadataMock); + + $fileUploader = $this->getMockBuilder('Magento\Customer\Model\FileUploader') + ->disableOriginalConstructor() + ->getMock(); + $fileUploader->expects($this->once()) + ->method('validate') + ->willReturn($errors); + + $this->fileUploaderFactory->expects($this->once()) + ->method('create') + ->with([ + 'attributeMetadata' => $attributeMetadataMock, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'scope' => 'customer', + ]) + ->willReturn($fileUploader); + + $resultJson = $this->getMockBuilder('Magento\Framework\Controller\Result\Json') + ->disableOriginalConstructor() + ->getMock(); + $resultJson->expects($this->once()) + ->method('setData') + ->with([ + 'error' => implode('
', $errors), + 'errorcode' => 0, + ]) + ->willReturnSelf(); + + $this->resultFactory->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($resultJson); + + $this->assertInstanceOf('Magento\Framework\Controller\Result\Json', $this->controller->execute()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index be09128d75717..10f8c912fba6d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -10,7 +10,6 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Ui\DataProvider\EavValidationRules; -use Magento\Customer\Model\Customer\DataProvider; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; @@ -104,6 +103,12 @@ public function testGetAttributesMetaWithOptions(array $expected) ] ); + $this->setBackwardCompatibleProperty( + $dataProvider, + 'fileProcessorFactory', + $this->fileProcessorFactory + ); + $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); $this->assertEquals($expected, $meta); @@ -334,6 +339,13 @@ public function testGetData() 'eavConfig' => $this->getEavConfigMock() ] ); + + $this->setBackwardCompatibleProperty( + $dataProvider, + 'fileProcessorFactory', + $this->fileProcessorFactory + ); + $this->assertEquals( [ '' => [ @@ -364,16 +376,21 @@ public function testGetDataWithCustomAttributeImage() { $customerId = 1; $customerEmail = 'user1@example.com'; + $filename = '/filename.ext1'; $viewUrl = 'viewUrl'; + $expectedData = [ $customerId => [ 'customer' => [ 'email' => $customerEmail, 'img1' => [ - 'file' => $filename, - 'type' => 'image', - 'url' => $viewUrl, + [ + 'file' => $filename, + 'size' => 1, + 'url' => $viewUrl, + 'name' => 'filename.ext1', + ], ], ], ], @@ -401,7 +418,10 @@ public function testGetDataWithCustomAttributeImage() ->getMock(); $customerMock->expects($this->once()) ->method('getData') - ->willReturn(['email' => $customerEmail, 'img1' => $filename]); + ->willReturn([ + 'email' => $customerEmail, + 'img1' => $filename, + ]); $customerMock->expects($this->once()) ->method('getAddresses') ->willReturn([]); @@ -435,6 +455,10 @@ public function testGetDataWithCustomAttributeImage() ->method('isExist') ->with($filename) ->willReturn(true); + $this->fileProcessor->expects($this->once()) + ->method('getStat') + ->with($filename) + ->willReturn(['size' => 1]); $this->fileProcessor->expects($this->once()) ->method('getViewUrl') ->with('/filename.ext1', 'image') @@ -538,6 +562,145 @@ public function testGetDataWithCustomAttributeImageNoData() $this->assertEquals($expectedData, $dataProvider->getData()); } + public function testGetAttributesMetaWithCustomAttributeImage() + { + $maxFileSize = 1000; + $allowedExtension = 'ext1 ext2'; + + $attributeCode = 'img1'; + + $collectionMock = $this->getMockBuilder('Magento\Customer\Model\ResourceModel\Customer\Collection') + ->disableOriginalConstructor() + ->getMock(); + $collectionMock->expects($this->once()) + ->method('addAttributeToSelect') + ->with('*'); + + $this->customerCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + + $attributeMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Attribute\AbstractAttribute') + ->setMethods([ + 'getAttributeCode', + 'getFrontendInput', + 'getDataUsingMethod', + ]) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $attributeMock->expects($this->any()) + ->method('getFrontendInput') + ->willReturn('image'); + $attributeMock->expects($this->any()) + ->method('getDataUsingMethod') + ->willReturnCallback( + function ($origName) { + return $origName; + } + ); + + $typeCustomerMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Type') + ->disableOriginalConstructor() + ->getMock(); + $typeCustomerMock->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn([$attributeMock]); + $typeCustomerMock->expects($this->once()) + ->method('getEntityTypeCode') + ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $typeAddressMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Type') + ->disableOriginalConstructor() + ->getMock(); + $typeAddressMock->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn([]); + + $this->eavConfigMock->expects($this->at(0)) + ->method('getEntityType') + ->with('customer') + ->willReturn($typeCustomerMock); + $this->eavConfigMock->expects($this->at(1)) + ->method('getEntityType') + ->with('customer_address') + ->willReturn($typeAddressMock); + + $this->eavValidationRulesMock->expects($this->once()) + ->method('build') + ->with($attributeMock, [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'visible' => 'is_visible', + 'required' => 'is_required', + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'label' => __('frontend_label'), + ]) + ->willReturn([ + 'max_file_size' => $maxFileSize, + 'file_extensions' => 'ext1, eXt2 ', // Added spaces and upper-cases + ]); + + $this->fileProcessorFactory->expects($this->any()) + ->method('create') + ->with([ + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]) + ->willReturn($this->fileProcessor); + + $objectManager = new ObjectManager($this); + $dataProvider = $objectManager->getObject( + '\Magento\Customer\Model\Customer\DataProvider', + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'eavValidationRules' => $this->eavValidationRulesMock, + 'customerCollectionFactory' => $this->customerCollectionFactoryMock, + 'eavConfig' => $this->eavConfigMock, + 'fileProcessorFactory' => $this->fileProcessorFactory, + ] + ); + + $result = $dataProvider->getMeta(); + + $this->assertNotEmpty($result); + + $expected = [ + 'customer' => [ + 'fields' => [ + $attributeCode => [ + 'formElement' => 'fileUploader', + 'componentType' => 'fileUploader', + 'maxFileSize' => $maxFileSize, + 'allowedExtensions' => $allowedExtension, + 'uploaderConfig' => [ + 'url' => 'customer/file/customer_upload', + ], + 'sortOrder' => 'sort_order', + 'required' => 'is_required', + 'visible' => 'is_visible', + 'validation' => [ + 'max_file_size' => $maxFileSize, + 'file_extensions' => 'ext1, eXt2 ', + ], + 'label' => __('frontend_label'), + ], + ], + ], + 'address' => [ + 'fields' => [], + ], + ]; + + $this->assertEquals($expected, $result); + } + /** * Set mocked property * diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index ef36561d2ff66..eaa2afc70cf60 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -17,6 +17,11 @@ class FileProcessorTest extends \PHPUnit_Framework_TestCase */ private $filesystem; + /** + * @var \Magento\MediaStorage\Model\File\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uploaderFactory; + /** * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -45,6 +50,11 @@ protected function setUp() ->with(DirectoryList::MEDIA) ->willReturn($this->mediaDirectory); + $this->uploaderFactory = $this->getMockBuilder('Magento\MediaStorage\Model\File\UploaderFactory') + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->urlBuilder = $this->getMockBuilder('Magento\Framework\UrlInterface') ->getMockForAbstractClass(); @@ -54,20 +64,39 @@ protected function setUp() /** * @param string $entityTypeCode + * @param array $allowedExtensions * @return FileProcessor */ - private function getModel($entityTypeCode) + private function getModel($entityTypeCode, array $allowedExtensions = []) { $model = new FileProcessor( $this->filesystem, + $this->uploaderFactory, $this->urlBuilder, $this->urlEncoder, - $entityTypeCode + $entityTypeCode, + $allowedExtensions ); - return $model; } + public function testGetStat() + { + $fileName = '/filename.ext1'; + + $this->mediaDirectory->expects($this->once()) + ->method('stat') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . $fileName) + ->willReturn(['size' => 1]); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $result = $model->getStat($fileName); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('size', $result); + $this->assertEquals(1, $result['size']); + } + public function testIsExist() { $fileName = '/filename.ext1'; @@ -77,15 +106,7 @@ public function testIsExist() ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . $fileName) ->willReturn(true); - $this->model = new FileProcessor( - $this->filesystem, - $this->urlBuilder, - $this->urlEncoder, - CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER - ); - $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - $this->assertTrue($model->isExist($fileName)); } @@ -107,7 +128,6 @@ public function testGetViewUrlCustomer() ->willReturn($fileUrl); $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - $this->assertEquals($fileUrl, $model->getViewUrl($filePath, 'image')); } @@ -129,7 +149,236 @@ public function testGetViewUrlCustomerAddress() ->willReturn($relativeUrl); $model = $this->getModel(AddressMetadataInterface::ENTITY_TYPE_ADDRESS); - $this->assertEquals($baseUrl . $relativeUrl, $model->getViewUrl($filePath, 'image')); } + + public function testRemoveUploadedFile() + { + $fileName = '/filename.ext1'; + + $this->mediaDirectory->expects($this->once()) + ->method('delete') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . $fileName) + ->willReturn(true); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $this->assertTrue($model->removeUploadedFile($fileName)); + } + + public function testSaveTemporaryFile() + { + $attributeCode = 'img1'; + + $allowedExtensions = [ + 'ext1', + 'ext2', + ]; + + $absolutePath = '/absolute/filepath'; + + $expectedResult = [ + 'file' => 'filename.ext1', + 'path' => 'filepath', + ]; + + $uploaderMock = $this->getMockBuilder('Magento\MediaStorage\Model\File\Uploader') + ->disableOriginalConstructor() + ->getMock(); + $uploaderMock->expects($this->once()) + ->method('setFilesDispersion') + ->with(false) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setFilenamesCaseSensitivity') + ->with(false) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setAllowRenameFiles') + ->with(true) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setAllowedExtensions') + ->with($allowedExtensions) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('save') + ->with($absolutePath) + ->willReturn($expectedResult); + + $this->uploaderFactory->expects($this->once()) + ->method('create') + ->with(['fileId' => 'customer[' . $attributeCode . ']']) + ->willReturn($uploaderMock); + + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR) + ->willReturn($absolutePath); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $allowedExtensions); + $result = $model->saveTemporaryFile('customer[' . $attributeCode . ']'); + + $this->assertEquals($expectedResult, $result); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage File can not be saved to the destination folder. + */ + public function testSaveTemporaryFileWithError() + { + $attributeCode = 'img1'; + + $allowedExtensions = [ + 'ext1', + 'ext2', + ]; + + $absolutePath = '/absolute/filepath'; + + $uploaderMock = $this->getMockBuilder('Magento\MediaStorage\Model\File\Uploader') + ->disableOriginalConstructor() + ->getMock(); + $uploaderMock->expects($this->once()) + ->method('setFilesDispersion') + ->with(false) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setFilenamesCaseSensitivity') + ->with(false) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setAllowRenameFiles') + ->with(true) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('setAllowedExtensions') + ->with($allowedExtensions) + ->willReturnSelf(); + $uploaderMock->expects($this->once()) + ->method('save') + ->with($absolutePath) + ->willReturn(false); + + $this->uploaderFactory->expects($this->once()) + ->method('create') + ->with(['fileId' => 'customer[' . $attributeCode . ']']) + ->willReturn($uploaderMock); + + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR) + ->willReturn($absolutePath); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $allowedExtensions); + $model->saveTemporaryFile('customer[' . $attributeCode . ']'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Unable to create directory customer/f/i + */ + public function testMoveTemporaryFileUnableToCreateDirectory() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(false); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $model->moveTemporaryFile($filePath); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Destination folder is not writable or does not exists + */ + public function testMoveTemporaryFileDestinationFolderDoesNotExists() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(false); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $model->moveTemporaryFile($filePath); + } + + public function testMoveTemporaryFile() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with($destinationPath) + ->willReturn('/' . $destinationPath); + + $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; + $newPath = $destinationPath . $filePath; + + $this->mediaDirectory->expects($this->once()) + ->method('renameFile') + ->with($path, $newPath) + ->willReturn(true); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $this->assertEquals('/f/i' . $filePath, $model->moveTemporaryFile($filePath)); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Something went wrong while saving the file + */ + public function testMoveTemporaryFileWithException() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with($destinationPath) + ->willReturn('/' . $destinationPath); + + $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; + $newPath = $destinationPath . $filePath; + + $this->mediaDirectory->expects($this->once()) + ->method('renameFile') + ->with($path, $newPath) + ->willThrowException(new \Exception('Exception.')); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $model->moveTemporaryFile($filePath); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php new file mode 100644 index 0000000000000..a902151860f8f --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php @@ -0,0 +1,186 @@ +customerMetadataService = $this->getMockBuilder('Magento\Customer\Api\CustomerMetadataInterface') + ->getMockForAbstractClass(); + + $this->addressMetadataService = $this->getMockBuilder('Magento\Customer\Api\AddressMetadataInterface') + ->getMockForAbstractClass(); + + $this->elementFactory = $this->getMockBuilder('Magento\Customer\Model\Metadata\ElementFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->fileProcessorFactory = $this->getMockBuilder('Magento\Customer\Model\FileProcessorFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->attributeMetadata = $this->getMockBuilder('Magento\Customer\Api\Data\AttributeMetadataInterface') + ->getMockForAbstractClass(); + } + + /** + * @param string $entityTypeCode + * @param string $scope + * @return FileUploader + */ + private function getModel($entityTypeCode, $scope) + { + $model = new FileUploader( + $this->customerMetadataService, + $this->addressMetadataService, + $this->elementFactory, + $this->fileProcessorFactory, + $this->attributeMetadata, + $entityTypeCode, + $scope + ); + return $model; + } + + public function testValidate() + { + $attributeCode = 'attribute_code'; + + $filename = 'filename.ext1'; + + $_FILES = [ + 'customer' => [ + 'name' => [ + $attributeCode => $filename, + ], + ], + ]; + + $formElement = $this->getMockBuilder('Magento\Customer\Model\Metadata\Form\Image') + ->disableOriginalConstructor() + ->getMock(); + $formElement->expects($this->once()) + ->method('validateValue') + ->with(['name' => $filename]) + ->willReturn(true); + + $this->elementFactory->expects($this->once()) + ->method('create') + ->with($this->attributeMetadata, null, CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) + ->willReturn($formElement); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'customer'); + $this->assertTrue($model->validate()); + } + + public function testUpload() + { + $attributeCode = 'attribute_code'; + $attributeFrontendInput = 'image'; + + $resultFileName = '/filename.ext1'; + $resultFilePath = 'filepath'; + $resultFileUrl = 'viewFileUrl'; + + $allowedExtensions = 'ext1,EXT2 , eXt3'; // Added spaces, commas and upper-cases + $expectedAllowedExtensions = [ + 'ext1', + 'ext2', + 'ext3', + ]; + + $_FILES = [ + 'customer' => [ + 'name' => [ + $attributeCode => 'filename', + ], + ], + ]; + + $expectedResult = [ + 'name' => $resultFileName, + 'file' => $resultFileName, + 'path' => $resultFilePath, + 'tmp_name' => $resultFilePath . $resultFileName, + 'url' => $resultFileUrl, + ]; + + $fileProcessor = $this->getMockBuilder('Magento\Customer\Model\FileProcessor') + ->disableOriginalConstructor() + ->getMock(); + $fileProcessor->expects($this->once()) + ->method('saveTemporaryFile') + ->with('customer[' . $attributeCode . ']') + ->willReturn([ + 'name' => $resultFileName, + 'path' => $resultFilePath, + 'file' => $resultFileName, + ]); + $fileProcessor->expects($this->once()) + ->method('getViewUrl') + ->with(FileProcessor::TMP_DIR . '/filename.ext1', $attributeFrontendInput) + ->willReturn($resultFileUrl); + + $this->fileProcessorFactory->expects($this->once()) + ->method('create') + ->with([ + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'allowedExtensions' => $expectedAllowedExtensions, + ]) + ->willReturn($fileProcessor); + + $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') + ->getMockForAbstractClass(); + $validationRuleMock->expects($this->once()) + ->method('getName') + ->willReturn('file_extensions'); + $validationRuleMock->expects($this->once()) + ->method('getValue') + ->willReturn($allowedExtensions); + + $this->attributeMetadata->expects($this->once()) + ->method('getFrontendInput') + ->willReturn($attributeFrontendInput); + $this->attributeMetadata->expects($this->once()) + ->method('getValidationRules') + ->willReturn([$validationRuleMock]); + + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'customer'); + $this->assertEquals($expectedResult, $model->upload()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php index 179d49a441bce..18edde338ed8b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php @@ -276,6 +276,7 @@ public function validateValueToUploadDataProvider() ['tmp_name' => 'tempName_0001.bin', 'name' => 'realFileName.bin'], ['uploaded' => false], ], + 'isValid' => [true, ['tmp_name' => 'tempName_0001.txt', 'name' => 'realFileName.txt']], ]; } @@ -300,7 +301,12 @@ public function testCompactValueNoDelete() 'entityTypeCode' => self::ENTITY_TYPE, ]); - $this->assertSame('value', $model->compactValue([])); + $this->fileProcessorMock->expects($this->once()) + ->method('removeUploadedFile') + ->with('value') + ->willReturnSelf(); + + $this->assertSame([], $model->compactValue([])); } public function testCompactValueDelete() @@ -446,6 +452,77 @@ public function testCompactValueNoAction() $this->assertEquals($value, $model->compactValue($value)); } + public function testCompactValueUiComponent() + { + $value = [ + 'file' => 'filename', + ]; + + $model = $this->initialize([ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->fileProcessorMock->expects($this->once()) + ->method('moveTemporaryFile') + ->with($value['file']) + ->willReturn(true); + + $this->assertTrue($model->compactValue($value)); + } + + public function testCompactValueRemoveUiComponentValue() + { + $value = 'value'; + + $model = $this->initialize([ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $this->fileProcessorMock->expects($this->once()) + ->method('removeUploadedFile') + ->with($value) + ->willReturnSelf(); + + $this->assertEquals([], $model->compactValue([])); + } + + public function testExtractValueFileUploaderUIComponent() + { + $attributeCode = 'img1'; + $requestScope = 'customer'; + $fileName = 'filename.ext1'; + + $this->attributeMetadataMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with($requestScope) + ->willReturn([ + $attributeCode => [ + [ + 'file' => $fileName, + ], + ], + ]); + + $model = $this->initialize([ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ]); + + $model->setRequestScope($requestScope); + $result = $model->extractValue($this->requestMock); + + $this->assertEquals(['file' => $fileName], $result); + } + public function testCompactValueInputField() { $value = [ @@ -566,6 +643,11 @@ private function initialize(array $data) $this->uploaderFactoryMock ); + $reflection = new \ReflectionClass(get_class($model)); + $reflectionProperty = $reflection->getProperty('fileProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($model, $this->fileProcessorMock); + return $model; } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php index 6daf1543e5507..24382c2b403c7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php @@ -31,6 +31,16 @@ class ImageTest extends AbstractFormTestCase */ protected $uploaderFactoryMock; + /** + * @var FileProcessor|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileProcessorMock; + + /** + * @var \Magento\Framework\Api\Data\ImageContentInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $imageContentFactory; + protected function setUp() { parent::setUp(); @@ -52,6 +62,15 @@ protected function setUp() $this->uploaderFactoryMock = $this->getMockBuilder('Magento\Framework\File\UploaderFactory') ->disableOriginalConstructor() ->getMock(); + + $this->fileProcessorMock = $this->getMockBuilder('Magento\Customer\Model\FileProcessor') + ->disableOriginalConstructor() + ->getMock(); + + $this->imageContentFactory = $this->getMockBuilder('Magento\Framework\Api\Data\ImageContentInterfaceFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); } /** @@ -60,24 +79,30 @@ protected function setUp() */ private function initialize(array $data) { - $model = $this->getMock( - 'Magento\Customer\Model\Metadata\Form\Image', - ['_isUploadedFile'], - [ - $this->localeMock, - $this->loggerMock, - $this->attributeMetadataMock, - $this->localeResolverMock, - $data['value'], - $data['entityTypeCode'], - $data['isAjax'], - $this->urlEncode, - $this->fileValidatorMock, - $this->fileSystemMock, - $this->uploaderFactoryMock - ] + $model = new \Magento\Customer\Model\Metadata\Form\Image( + $this->localeMock, + $this->loggerMock, + $this->attributeMetadataMock, + $this->localeResolverMock, + $data['value'], + $data['entityTypeCode'], + $data['isAjax'], + $this->urlEncode, + $this->fileValidatorMock, + $this->fileSystemMock, + $this->uploaderFactoryMock ); + $reflection = new \ReflectionClass(get_class($model)); + $reflectionProperty = $reflection->getProperty('fileProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($model, $this->fileProcessorMock); + + $reflection = new \ReflectionClass(get_class($model)); + $reflectionProperty = $reflection->getProperty('imageContentFactory'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($model, $this->imageContentFactory); + return $model; } @@ -92,6 +117,11 @@ public function testValidateIsNotValidFile() ->method('getStoreLabel') ->willReturn('File Input Field Label'); + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['tmp_name']) + ->willReturn(true); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -112,16 +142,17 @@ public function testValidate() ->method('getStoreLabel') ->willReturn('File Input Field Label'); + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['name']) + ->willReturn(true); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, ]); - $model->expects($this->any()) - ->method('_isUploadedFile') - ->will($this->returnValue($value['tmp_name'])); - $this->assertTrue($model->validateValue($value)); } @@ -148,6 +179,11 @@ public function testCompactValueUiComponentCustomerNotExists() 'type' => 'image', ]; + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->willReturn(false); + $model = $this->initialize([ 'value' => $originValue, 'isAjax' => false, @@ -167,8 +203,9 @@ public function testValidateMaxFileSize() $maxFileSize = 1; - $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') - ->getMockForAbstractClass(); + $validationRuleMock = $this->getMockBuilder( + \Magento\Customer\Api\Data\ValidationRuleInterface::class + )->getMockForAbstractClass(); $validationRuleMock->expects($this->any()) ->method('getName') ->willReturn('max_file_size'); @@ -183,16 +220,17 @@ public function testValidateMaxFileSize() ->method('getValidationRules') ->willReturn([$validationRuleMock]); + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['name']) + ->willReturn(true); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, ]); - $model->expects($this->any()) - ->method('_isUploadedFile') - ->will($this->returnValue($value['tmp_name'])); - $this->assertEquals(['"logo.gif" exceeds the allowed file size.'], $model->validateValue($value)); } @@ -205,8 +243,9 @@ public function testValidateMaxImageWidth() $maxImageWidth = 1; - $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') - ->getMockForAbstractClass(); + $validationRuleMock = $this->getMockBuilder( + \Magento\Customer\Api\Data\ValidationRuleInterface::class + )->getMockForAbstractClass(); $validationRuleMock->expects($this->any()) ->method('getName') ->willReturn('max_image_width'); @@ -221,16 +260,17 @@ public function testValidateMaxImageWidth() ->method('getValidationRules') ->willReturn([$validationRuleMock]); + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['name']) + ->willReturn(true); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, ]); - $model->expects($this->any()) - ->method('_isUploadedFile') - ->will($this->returnValue($value['tmp_name'])); - $this->assertEquals(['"logo.gif" width exceeds allowed value of 1 px.'], $model->validateValue($value)); } @@ -243,8 +283,9 @@ public function testValidateMaxImageHeight() $maxImageHeight = 1; - $validationRuleMock = $this->getMockBuilder('Magento\Customer\Api\Data\ValidationRuleInterface') - ->getMockForAbstractClass(); + $validationRuleMock = $this->getMockBuilder( + \Magento\Customer\Api\Data\ValidationRuleInterface::class + )->getMockForAbstractClass(); $validationRuleMock->expects($this->any()) ->method('getName') ->willReturn('max_image_heght'); @@ -259,16 +300,17 @@ public function testValidateMaxImageHeight() ->method('getValidationRules') ->willReturn([$validationRuleMock]); + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['name']) + ->willReturn(true); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, ]); - $model->expects($this->any()) - ->method('_isUploadedFile') - ->will($this->returnValue($value['tmp_name'])); - $this->assertEquals(['"logo.gif" height exceeds allowed value of 1 px.'], $model->validateValue($value)); } @@ -288,4 +330,80 @@ public function testCompactValueNoChanges() $this->assertEquals($originValue, $model->compactValue($value)); } + + public function testCompactValueUiComponentAddress() + { + $originValue = 'filename.ext1'; + + $value = [ + 'file' => 'filename.ext2', + ]; + + $this->fileProcessorMock->expects($this->once()) + ->method('moveTemporaryFile') + ->with($value['file']) + ->willReturn(true); + + $model = $this->initialize([ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + ]); + + $this->assertTrue($model->compactValue($value)); + } + + public function testCompactValueUiComponentCustomer() + { + $originValue = 'filename.ext1'; + + $value = [ + 'file' => 'filename.ext2', + 'name' => 'filename.ext2', + 'type' => 'image', + ]; + + $base64EncodedData = 'encoded_data'; + + $this->fileProcessorMock->expects($this->once()) + ->method('isExist') + ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->willReturn(true); + $this->fileProcessorMock->expects($this->once()) + ->method('getBase64EncodedData') + ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->willReturn($base64EncodedData); + $this->fileProcessorMock->expects($this->once()) + ->method('removeUploadedFile') + ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->willReturnSelf(); + + $imageContentMock = $this->getMockBuilder( + \Magento\Framework\Api\Data\ImageContentInterface::class + )->getMockForAbstractClass(); + $imageContentMock->expects($this->once()) + ->method('setName') + ->with($value['name']) + ->willReturnSelf(); + $imageContentMock->expects($this->once()) + ->method('setBase64EncodedData') + ->with($base64EncodedData) + ->willReturnSelf(); + $imageContentMock->expects($this->once()) + ->method('setType') + ->with($value['type']) + ->willReturnSelf(); + + $this->imageContentFactory->expects($this->once()) + ->method('create') + ->willReturn($imageContentMock); + + $model = $this->initialize([ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + ]); + + $this->assertEquals($imageContentMock, $model->compactValue($value)); + } } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php index 264b948f34b89..153aea3ebc36a 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php @@ -21,4 +21,28 @@ public function getComponentName() { return static::NAME; } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + if ($this->getData('config/uploaderConfig/url')) { + $url = $this->getContext()->getUrl($this->getData('config/uploaderConfig/url'), ['_secure' => true]); + $data = array_replace_recursive( + $this->getData(), + [ + 'config' => [ + 'uploaderConfig' => [ + 'url' => $url + ], + ], + ] + ); + $this->setData($data); + } + parent::prepare(); + } } diff --git a/app/code/Magento/Ui/DataProvider/EavValidationRules.php b/app/code/Magento/Ui/DataProvider/EavValidationRules.php index 6973a2421f4e6..e43de74fecfff 100644 --- a/app/code/Magento/Ui/DataProvider/EavValidationRules.php +++ b/app/code/Magento/Ui/DataProvider/EavValidationRules.php @@ -15,11 +15,9 @@ class EavValidationRules /** * @var array */ - protected $validationRul = [ - 'input_validation' => [ - 'email' => ['validate-email' => true], - 'date' => ['validate-date' => true], - ], + protected $validationRules = [ + 'email' => ['validate-email' => true], + 'date' => ['validate-date' => true], ]; /** @@ -31,26 +29,23 @@ class EavValidationRules */ public function build(AbstractAttribute $attribute, array $data) { - $rules = []; + $validation = []; if (isset($data['required']) && $data['required'] == 1) { - $rules['required-entry'] = true; + $validation = array_merge($validation, ['required-entry' => true]); } - $validation = $attribute->getValidateRules(); - if (!empty($validation)) { - foreach ($validation as $type => $ruleName) { - switch ($type) { - case 'input_validation': - if (isset($this->validationRul[$type][$ruleName])) { - $rules = array_merge($rules, $this->validationRul[$type][$ruleName]); - } - break; - case 'min_text_length': - case 'max_text_length': - $rules = array_merge($rules, [$type => $ruleName]); - break; - } - + if ($attribute->getFrontendInput() === 'price') { + $validation = array_merge($validation, ['validate-zero-or-greater' => true]); + } + if ($attribute->getValidateRules()) { + $validation = array_merge($validation, $attribute->getValidateRules()); + } + $rules = []; + foreach ($validation as $type => $ruleName) { + $rule = [$type => $ruleName]; + if ($type === 'input_validation') { + $rule = isset($this->validationRules[$ruleName]) ? $this->validationRules[$ruleName] : []; } + $rules = array_merge($rules, $rule); } return $rules; diff --git a/lib/internal/Magento/Framework/Api/ImageProcessor.php b/lib/internal/Magento/Framework/Api/ImageProcessor.php index c3f479620233b..28dbb8c83885c 100644 --- a/lib/internal/Magento/Framework/Api/ImageProcessor.php +++ b/lib/internal/Magento/Framework/Api/ImageProcessor.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Api; +use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\InputException; use Magento\Framework\Filesystem; @@ -103,7 +104,7 @@ public function save( /** @var $imageDataObject \Magento\Framework\Api\AttributeValue */ foreach ($imageDataObjects as $imageDataObject) { - /** @var $imageContent \Magento\Framework\Api\Data\ImageContentInterface */ + /** @var $imageContent ImageContentInterface */ $imageContent = $imageDataObject->getValue(); $filename = $this->processImageContent($entityType, $imageContent); diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/Api/ImageProcessorTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/Api/ImageProcessorTest.php index b25569db2fb4a..b88d100f1cb13 100644 --- a/lib/internal/Magento/Framework/Api/Test/Unit/Api/ImageProcessorTest.php +++ b/lib/internal/Magento/Framework/Api/Test/Unit/Api/ImageProcessorTest.php @@ -162,6 +162,9 @@ public function testSaveWithNoPreviousData() $imageContent->expects($this->any()) ->method('getName') ->willReturn('testFileName'); + $imageContent->expects($this->any()) + ->method('getType') + ->willReturn('image/jpg'); $imageDataObject = $this->getMockBuilder('Magento\Framework\Api\AttributeValue') ->disableOriginalConstructor() @@ -201,6 +204,9 @@ public function testSaveWithPreviousData() $imageContent->expects($this->any()) ->method('getName') ->willReturn('testFileName'); + $imageContent->expects($this->any()) + ->method('getType') + ->willReturn('image/jpg'); $imageDataObject = $this->getMockBuilder('Magento\Framework\Api\AttributeValue') ->disableOriginalConstructor() From 4040a02faf960abc3112d6d5b4e298585c915624 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 19 Oct 2016 17:59:29 +0300 Subject: [PATCH 29/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Form/Element/DataType/MediaTest.php | 58 ++++++++++++++ .../DataProvider/EavValidationRulesTest.php | 80 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php create mode 100644 app/code/Magento/Ui/Test/Unit/DataProvider/EavValidationRulesTest.php diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php new file mode 100644 index 0000000000000..75c7eaae58036 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php @@ -0,0 +1,58 @@ +context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $this->urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) + ->getMockForAbstractClass(); + $this->processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->any()) + ->method('getProcessor') + ->willReturn($this->processor); + + $this->media = new Media($this->context); + $this->media->setData( + [ + 'config' => [ + 'uploaderConfig' => [ + 'url' => 'module/actionPath/path' + ], + ], + ] + ); + } + + public function testPrepare() + { + $url = 'http://magento2.com/module/actionPath/path/key/34523456234523trdg'; + $this->context->expects($this->once()) + ->method('getUrl') + ->with('module/actionPath/path', ['_secure' => true]) + ->willReturn($url); + $this->media->prepare(); + } +} diff --git a/app/code/Magento/Ui/Test/Unit/DataProvider/EavValidationRulesTest.php b/app/code/Magento/Ui/Test/Unit/DataProvider/EavValidationRulesTest.php new file mode 100644 index 0000000000000..ec5f8a49cedf6 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/DataProvider/EavValidationRulesTest.php @@ -0,0 +1,80 @@ +objectManager = new ObjectManager($this); + $this->attributeMock = + $this->getMockBuilder(AbstractAttribute::class) + ->setMethods(['getFrontendInput', 'getValidateRules']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->subject = new EavValidationRules(); + } + + /** + * @param array $data + * @param array $expected + * @dataProvider buildDataProvider + */ + public function testBuild($attributeInputType, $validateRules, $data, $expected) + { + $this->attributeMock->expects($this->once())->method('getFrontendInput')->willReturn($attributeInputType); + $this->attributeMock->expects($this->any())->method('getValidateRules')->willReturn($validateRules); + $validationRules = $this->subject->build($this->attributeMock, $data); + $this->assertEquals($expected, $validationRules); + } + + /** + * @return array + */ + public function buildDataProvider() + { + return [ + ['', '', [], []], + ['', null, [], []], + ['', false, [], []], + ['', [], [], []], + ['', '', ['required' => 1], ['required-entry' => true]], + ['price', '', [], ['validate-zero-or-greater' => true]], + ['price', '', ['required' => 1], ['validate-zero-or-greater' => true, 'required-entry' => true]], + ['', ['input_validation' => 'email'], [], ['validate-email' => true]], + ['', ['input_validation' => 'date'], [], ['validate-date' => true]], + ['', ['input_validation' => 'other'], [], []], + ['', ['max_text_length' => '254'], ['required' => 1], ['max_text_length' => 254, 'required-entry' => true]], + ['', ['max_text_length' => '254', 'min_text_length' => 1], [], + ['max_text_length' => 254, 'min_text_length' => 1]], + ['', ['max_text_length' => '254', 'input_validation' => 'date'], [], + ['max_text_length' => 254, 'validate-date' => true]], + ]; + } +} From 19164c0013e25023d6159537a30b0e62c739804c Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 19 Oct 2016 19:35:41 +0300 Subject: [PATCH 30/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Adminhtml/File/Address/Upload.php | 2 +- .../Customer/Model/Customer/DataProvider.php | 37 +++--- .../Magento/Customer/Model/FileProcessor.php | 3 + .../Unit/Model/Customer/DataProviderTest.php | 115 +++++++++--------- .../Magento/Framework/Api/ImageProcessor.php | 10 +- 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php index d7f83ba6a6380..4dead149a6016 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php @@ -114,7 +114,7 @@ public function execute() */ private function convertFilesArray() { - foreach($_FILES['address'] as $itemKey => $item) { + foreach ($_FILES['address'] as $itemKey => $item) { foreach ($item as $value) { if (is_array($value)) { $_FILES['address'][$itemKey] = [ diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 87805506e2262..4c474bf07ea59 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -16,7 +16,6 @@ use Magento\Customer\Model\Address; use Magento\Customer\Model\Customer; use Magento\Framework\App\ObjectManager; -use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; @@ -216,24 +215,22 @@ private function getFileUploaderData( ? $customerData[$attributeCode] : ''; - /** @var FileProcessor $fileProcessor */ - $fileProcessor = $this->getFileProcessorFactory()->create([ - 'entityTypeCode' => $entityType->getEntityTypeCode(), - ]); - - if (!empty($file) - && $fileProcessor->isExist($file) - ) { - $stat = $fileProcessor->getStat($file); - $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); - } + if (!empty($file)) { + /** @var FileProcessor $fileProcessor */ + $fileProcessor = $this->getFileProcessorFactory()->create([ + 'entityTypeCode' => $entityType->getEntityTypeCode(), + ]); + + if ($fileProcessor->isExist($file)) { + $stat = $fileProcessor->getStat($file); + $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); + } - $fileName = $file; - if (strrpos($fileName, '/') !== false) { - $fileName = substr($fileName, strrpos($fileName, '/') + 1); - } + $fileName = $file; + if (strrpos($fileName, '/') !== false) { + $fileName = substr($fileName, strrpos($fileName, '/') + 1); + } - if (!empty($file)) { return [ [ 'file' => $file, @@ -243,6 +240,7 @@ private function getFileUploaderData( ], ]; } + return []; } @@ -292,6 +290,7 @@ protected function getAttributesMeta(Type $entityType) * @param Type $entityType * @param AbstractAttribute $attribute * @param array $config + * @return void */ private function overrideFileUploaderMetadata( Type $entityType, @@ -309,7 +308,9 @@ private function overrideFileUploaderMetadata( if (isset($config['validation']['file_extensions'])) { $allowedExtensions = explode(',', $config['validation']['file_extensions']); - array_walk($allowedExtensions, function(&$value) { $value = strtolower(trim($value)); }); + array_walk($allowedExtensions, function (&$value) { + $value = strtolower(trim($value)); + }); } $allowedExtensions = implode(' ', $allowedExtensions); diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index c98a85cc18463..f521753d133c7 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -16,6 +16,9 @@ use Magento\MediaStorage\Model\File\Uploader; use Magento\MediaStorage\Model\File\UploaderFactory; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class FileProcessor { /** diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 10f8c912fba6d..0c997378971bb 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Test\Unit\Model\Customer; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Customer\DataProvider; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -90,18 +91,10 @@ protected function setUp() */ public function testGetAttributesMetaWithOptions(array $expected) { - $helper = new ObjectManager($this); - $dataProvider = $helper->getObject( - '\Magento\Customer\Model\Customer\DataProvider', - [ - 'name' => 'test-name', - 'primaryFieldName' => 'primary-field-name', - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'eavConfig' => $this->getEavConfigMock() - ] - ); + $dataProvider = $this->getDataProvider([ + 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), + 'eavConfig' => $this->getEavConfigMock(), + ]); $this->setBackwardCompatibleProperty( $dataProvider, @@ -327,18 +320,9 @@ public function testGetData() ->method('getAttributes') ->willReturn([]); - $helper = new ObjectManager($this); - $dataProvider = $helper->getObject( - '\Magento\Customer\Model\Customer\DataProvider', - [ - 'name' => 'test-name', - 'primaryFieldName' => 'primary-field-name', - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() - ] - ); + $dataProvider = $this->getDataProvider([ + 'eavConfig' => $this->getEavConfigMock(), + ]); $this->setBackwardCompatibleProperty( $dataProvider, @@ -376,10 +360,8 @@ public function testGetDataWithCustomAttributeImage() { $customerId = 1; $customerEmail = 'user1@example.com'; - $filename = '/filename.ext1'; $viewUrl = 'viewUrl'; - $expectedData = [ $customerId => [ 'customer' => [ @@ -464,20 +446,11 @@ public function testGetDataWithCustomAttributeImage() ->with('/filename.ext1', 'image') ->willReturn($viewUrl); - $objectManager = new ObjectManager($this); - $dataProvider = $objectManager->getObject( - '\Magento\Customer\Model\Customer\DataProvider', - [ - 'name' => 'test-name', - 'primaryFieldName' => 'primary-field-name', - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() - ] - ); - $this->setBackwardCompatibleProperty($dataProvider, 'fileProcessorFactory', $this->fileProcessorFactory); + $dataProvider = $this->getDataProvider([ + 'eavConfig' => $this->getEavConfigMock(), + ]); + $this->setBackwardCompatibleProperty($dataProvider, 'fileProcessorFactory', $this->fileProcessorFactory); $this->assertEquals($expectedData, $dataProvider->getData()); } @@ -508,9 +481,6 @@ public function testGetDataWithCustomAttributeImageNoData() $entityTypeMock = $this->getMockBuilder('Magento\Eav\Model\Entity\Type') ->disableOriginalConstructor() ->getMock(); - $entityTypeMock->expects($this->once()) - ->method('getEntityTypeCode') - ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $customerMock = $this->getMockBuilder('Magento\Customer\Model\Customer') ->disableOriginalConstructor() @@ -653,24 +623,24 @@ function ($origName) { ]) ->willReturn($this->fileProcessor); - $objectManager = new ObjectManager($this); - $dataProvider = $objectManager->getObject( - '\Magento\Customer\Model\Customer\DataProvider', - [ - 'name' => 'test-name', - 'primaryFieldName' => 'primary-field-name', - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->eavConfigMock, - 'fileProcessorFactory' => $this->fileProcessorFactory, - ] - ); - + $dataProvider = $this->getDataProvider(); $result = $dataProvider->getMeta(); $this->assertNotEmpty($result); + $expected = $this->getExpected($attributeCode, $maxFileSize, $allowedExtension); + + $this->assertEquals($expected, $result); + } + + /** + * @param string $attributeCode + * @param int $maxFileSize + * @param string $allowedExtension + * @return array + */ + private function getExpected($attributeCode, $maxFileSize, $allowedExtension) + { $expected = [ 'customer' => [ 'fields' => [ @@ -697,8 +667,7 @@ function ($origName) { 'fields' => [], ], ]; - - $this->assertEquals($expected, $result); + return $expected; } /** @@ -716,4 +685,34 @@ public function setBackwardCompatibleProperty($object, $propertyName, $propertyV $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($object, $propertyValue); } + + /** + * @param array $params + * @return DataProvider + */ + private function getDataProvider(array $params = []) + { + $customerCollectionFactory = isset($params['customerCollectionFactory']) + ? $params['customerCollectionFactory'] + : $this->customerCollectionFactoryMock; + + $eavConfig = isset($params['eavConfig']) + ? $params['eavConfig'] + : $this->eavConfigMock; + + $objectManager = new ObjectManager($this); + $dataProvider = $objectManager->getObject( + DataProvider::class, + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'eavValidationRules' => $this->eavValidationRulesMock, + 'customerCollectionFactory' => $customerCollectionFactory, + 'eavConfig' => $eavConfig, + 'fileProcessorFactory' => $this->fileProcessorFactory, + ] + ); + return $dataProvider; + } } diff --git a/lib/internal/Magento/Framework/Api/ImageProcessor.php b/lib/internal/Magento/Framework/Api/ImageProcessor.php index 28dbb8c83885c..cb7deb00b3719 100644 --- a/lib/internal/Magento/Framework/Api/ImageProcessor.php +++ b/lib/internal/Magento/Framework/Api/ImageProcessor.php @@ -11,9 +11,11 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\Filesystem; use Magento\Framework\Phrase; +use Psr\Log\LoggerInterface; /** * Class ImageProcessor + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageProcessor implements ImageProcessorInterface { @@ -45,7 +47,7 @@ class ImageProcessor implements ImageProcessorInterface private $dataObjectHelper; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; @@ -63,14 +65,14 @@ class ImageProcessor implements ImageProcessorInterface * @param Filesystem $fileSystem * @param ImageContentValidatorInterface $contentValidator * @param DataObjectHelper $dataObjectHelper - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger * @param Uploader $uploader */ public function __construct( Filesystem $fileSystem, ImageContentValidatorInterface $contentValidator, DataObjectHelper $dataObjectHelper, - \Psr\Log\LoggerInterface $logger, + LoggerInterface $logger, Uploader $uploader ) { $this->filesystem = $fileSystem; @@ -92,7 +94,7 @@ public function save( //Get all Image related custom attributes $imageDataObjects = $this->dataObjectHelper->getCustomAttributeValueByType( $dataObjectWithCustomAttributes->getCustomAttributes(), - '\Magento\Framework\Api\Data\ImageContentInterface' + ImageContentInterface::class ); // Return if no images to process From d1712a73afa0778d6215f21e44de59a8bf1e07ef Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 19 Oct 2016 19:56:31 +0300 Subject: [PATCH 31/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Model/Customer/DataProvider.php | 21 +++++++++++++------ .../Unit/Model/Customer/DataProviderTest.php | 7 +------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 4c474bf07ea59..3ebe91f0ccef9 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -226,17 +226,12 @@ private function getFileUploaderData( $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); } - $fileName = $file; - if (strrpos($fileName, '/') !== false) { - $fileName = substr($fileName, strrpos($fileName, '/') + 1); - } - return [ [ 'file' => $file, 'size' => isset($stat) ? $stat['size'] : 0, 'url' => isset($viewUrl) ? $viewUrl : '', - 'name' => $fileName, + 'name' => $this->normalizeFileName($file), ], ]; } @@ -244,6 +239,20 @@ private function getFileUploaderData( return []; } + /** + * Normalize file name + * + * @param string $file + * @return string + */ + private function normalizeFileName($file) + { + if (strrpos($file, '/') !== false) { + $file = substr($file, strrpos($file, '/') + 1); + } + return $file; + } + /** * Get attributes meta * diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 0c997378971bb..347f2584ccb43 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -536,7 +536,6 @@ public function testGetAttributesMetaWithCustomAttributeImage() { $maxFileSize = 1000; $allowedExtension = 'ext1 ext2'; - $attributeCode = 'img1'; $collectionMock = $this->getMockBuilder('Magento\Customer\Model\ResourceModel\Customer\Collection') @@ -618,18 +617,14 @@ function ($origName) { $this->fileProcessorFactory->expects($this->any()) ->method('create') - ->with([ - 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - ]) + ->with(['entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER]) ->willReturn($this->fileProcessor); $dataProvider = $this->getDataProvider(); $result = $dataProvider->getMeta(); - $this->assertNotEmpty($result); $expected = $this->getExpected($attributeCode, $maxFileSize, $allowedExtension); - $this->assertEquals($expected, $result); } From e201711d4f9ffc29aea6427f8aa72c28c6e18616 Mon Sep 17 00:00:00 2001 From: Volodymyr Zaets Date: Thu, 20 Oct 2016 14:14:26 +0300 Subject: [PATCH 32/48] MAGETWO-56171: [Backport] File added via customer file (attachement) attribute is not uploaded / found --- app/code/Magento/Ui/etc/ui_components.xsd | 9 + app/code/Magento/Ui/etc/ui_definition.xsd | 1 + .../view/base/ui_component/etc/definition.xml | 11 + .../base/web/js/form/element/file-uploader.js | 417 ++++++++++++++++++ .../view/base/web/js/lib/validation/rules.js | 18 + .../form/element/uploader/preview.html | 59 +++ .../form/element/uploader/uploader.html | 81 ++++ .../backend/web/css/source/_components.less | 1 + .../css/source/components/_file-uploader.less | 290 ++++++++++++ 9 files changed, 887 insertions(+) create mode 100644 app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js create mode 100644 app/code/Magento/Ui/view/base/web/templates/form/element/uploader/preview.html create mode 100644 app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html create mode 100644 app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less diff --git a/app/code/Magento/Ui/etc/ui_components.xsd b/app/code/Magento/Ui/etc/ui_components.xsd index c98978098b9dd..8f2e2fcf87d23 100644 --- a/app/code/Magento/Ui/etc/ui_components.xsd +++ b/app/code/Magento/Ui/etc/ui_components.xsd @@ -201,6 +201,15 @@ + + + + + + + + + diff --git a/app/code/Magento/Ui/etc/ui_definition.xsd b/app/code/Magento/Ui/etc/ui_definition.xsd index e09f481207afa..d1c0f7ab74a71 100644 --- a/app/code/Magento/Ui/etc/ui_definition.xsd +++ b/app/code/Magento/Ui/etc/ui_definition.xsd @@ -40,6 +40,7 @@ + diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml b/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml index f3399fe88a8c3..7c0c839996fd1 100755 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml @@ -232,6 +232,17 @@ + + + + Magento_Ui/js/form/element/file-uploader + + ui/form/element/uploader/uploader + + + + + diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js new file mode 100644 index 0000000000000..7acd30bfe6c06 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -0,0 +1,417 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'mageUtils', + 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/lib/validation/validator', + 'Magento_Ui/js/form/element/abstract', + 'jquery/file-uploader' +], function ($, _, utils, uiAlert, validator, Element) { + 'use strict'; + + /** + * Constructor to creating valid validation format + * + * @param {(String|Object)} rule - One or many validation rules. + * @param {Boolean} passed. + * @param {String} msg - ErrorMessage + */ + var ValidationResult = function (rule, passed, msg) { + this.rule = rule || ''; + this.passed = passed || false; + this.msg = msg || ''; + }; + + return Element.extend({ + defaults: { + value: [], + maxFileSize: false, + isMultipleFiles: false, + allowedExtensions: false, + previewTmpl: 'ui/form/element/uploader/preview', + dropZone: '[data-role=drop-zone]', + isLoading: false, + uploaderConfig: { + dataType: 'json', + sequentialUploads: true, + formData: { + 'form_key': window.FORM_KEY + } + }, + tracks: { + isLoading: true + } + }, + + /** + * Initializes file uploader plugin on provided input element. + * + * @param {HTMLInputElement} fileInput + * @returns {FileUploader} Chainable. + */ + initUploader: function (fileInput) { + this.$fileInput = fileInput; + + _.extend(this.uploaderConfig, { + dropZone: $(fileInput).closest(this.dropZone), + change: this.onFilesChoosed.bind(this), + drop: this.onFilesChoosed.bind(this), + add: this.onBeforeFileUpload.bind(this), + done: this.onFileUploaded.bind(this), + start: this.onLoadingStart.bind(this), + stop: this.onLoadingStop.bind(this) + }); + + $(fileInput).fileupload(this.uploaderConfig); + + return this; + }, + + /** + * Defines initial value of the instance. + * + * @returns {FileUploader} Chainable. + */ + setInitialValue: function () { + var value = this.getInitialValue(); + + value = value.map(this.processFile, this); + + this.initialValue = value.slice(); + + this.value(value); + this.on('value', this.onUpdate.bind(this)); + + return this; + }, + + /** + * Empties files list. + * + * @returns {FileUploader} Chainable. + */ + clear: function () { + this.value.removeAll(); + + return this; + }, + + /** + * Checks if files list contains any items. + * + * @returns {Boolean} + */ + hasData: function () { + return !!this.value().length; + }, + + /** + * Resets files list to its' initial value. + * + * @returns {FileUploader} + */ + reset: function () { + var value = this.initialValue.slice(); + + this.value(value); + + return this; + }, + + /** + * Adds provided file to the files list. + * + * @param {Object} file + * @returns {FileUploder} Chainable. + */ + addFile: function (file) { + file = this.processFile(file); + + this.isMultipleFiles ? + this.value.push(file) : + this.value([file]); + + return this; + }, + + /** + * Retrieves from the list file which matches + * search criteria implemented in itertor function. + * + * @param {Function} fn - Function that will be invoked + * for each file in the list. + * @returns {Object} + */ + getFile: function (fn) { + return _.find(this.value(), fn); + }, + + /** + * Removes provided file from thes files list. + * + * @param {Object} file + * @returns {FileUploader} Chainable. + */ + removeFile: function (file) { + this.value.remove(file); + + return this; + }, + + /** + * May perform modifications on the provided + * file object before adding it to the files list. + * + * @param {Object} file + * @returns {Object} Modified file object. + */ + processFile: function (file) { + this.observe.call(file, true, [ + 'previewWidth', + 'previewHeight' + ]); + + return file; + }, + + /** + * Formats incoming bytes value to a readable format. + * + * @param {Number} bytes + * @returns {String} + */ + formatSize: function (bytes) { + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'], + i; + + if (bytes === 0) { + return '0 Byte'; + } + + i = window.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + + return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; + }, + + /** + * Returns path to the files' preview image. + * + * @param {Object} file + * @returns {String} + */ + getFilePreview: function (file) { + return file.url; + }, + + /** + * Returns path to the file's preview template. + * + * @returns {String} + */ + getPreviewTmpl: function () { + return this.previewTmpl; + }, + + /** + * Checks if provided file is allowed to be uploaded. + * + * @param {Object} file + * @returns {Object} Validation result. + */ + isFileAllowed: function (file) { + var result; + + _.every([ + this.isExtensionAllowed(file), + this.isSizeExceeded(file) + ], function (value) { + result = value; + + return value.passed; + }); + + return result; + }, + + /** + * Checks if extension of provided file is allowed. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isExtensionAllowed: function (file) { + return this.validatorResultParser('validate-file-type', file.name, this.allowedExtensions); + }, + + /** + * Checks if size of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isSizeExceeded: function (file) { + return this.validatorResultParser('validate-max-size', file.size, this.maxFileSize); + }, + + /** + * Parse validation string result to valid format + * + * @param {(String|Object)} rule - One or many validation rules. + * @param {*} value - Value to validate. + * @param {*} [params] - Rule configuration + * + * @returns {Object} Parsed validation result + */ + validatorResultParser: function (rule, value, params) { + var validation = validator(rule, value, params); + + return !validation ? new ValidationResult(rule, true) : new ValidationResult(rule, false, validation); + }, + + /** + * Displays provided error message. + * + * @param {String} msg + * @returns {FileUploader} Chainable. + */ + notifyError: function (msg) { + uiAlert({ + content: msg + }); + + return this; + }, + + /** + * Performs data type conversions. + * + * @param {*} value + * @returns {Array} + */ + normalizeData: function (value) { + return utils.isEmpty(value) ? [] : value; + }, + + /** + * Checks if files list is different + * from its' initial value. + * + * @returns {Boolean} + */ + hasChanged: function () { + var value = this.value(), + initial = this.initialValue; + + return !utils.equalArrays(value, initial); + }, + + /** + * Abstract handler which is invoked when files are choosed for upload. + * May be used for implementation of aditional validation rules, + * e.g. total files and a total size rules. + * + * @abstract + */ + onFilesChoosed: function () {}, + + /** + * Handler which is invoked prior to the start of a file upload. + * + * @param {Event} e - Event object. + * @param {Object} data - File data that will be uploaded. + */ + onBeforeFileUpload: function (e, data) { + var file = data.files[0], + allowed = this.isFileAllowed(file), + target = $(e.target); + + if (allowed.passed) { + target.on('fileuploadsend', function (event, postData) { + postData.data.set('param_name', this.paramName); + }.bind(data)); + + target.fileupload('process', data).done(function () { + data.submit(); + }); + } else { + this.notifyError(allowed.message); + } + }, + + /** + * Handler of the file upload complete event. + * + * @param {Event} e + * @param {Object} data + */ + onFileUploaded: function (e, data) { + var file = data.result, + error = file.error; + + error ? + this.notifyError(error) : + this.addFile(file); + }, + + /** + * Load start event handler. + */ + onLoadingStart: function () { + this.isLoading = true; + }, + + /** + * Load stop event handler. + */ + onLoadingStop: function () { + this.isLoading = false; + }, + + /** + * Handler function which is supposed to be invoked when + * file input element has been rendered. + * + * @param {HTMLInputElement} fileInput + */ + onElementRender: function (fileInput) { + this.initUploader(fileInput); + }, + + /** + * Handler of the preview image load event. + * + * @param {Object} file - File associated with an image. + * @param {Event} e + */ + onPreviewLoad: function (file, e) { + var img = e.currentTarget; + + file.previewWidth = img.naturalHeight; + file.previewHeight = img.naturalWidth; + }, + + /** + * Restore value to default + */ + restoreToDefault: function () { + var defaultValue = utils.copy(this.default); + + defaultValue.map(this.processFile, this); + this.value(defaultValue); + }, + + /** + * Update whether value differs from default value + */ + setDifferedFromDefault: function () { + var value = utils.copy(this.value()); + + this.isDifferedFromDefault(!_.isEqual(value, this.default)); + } + }); +}); \ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 0c9b5c1e37768..2fd5a0c8cdcff 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -787,6 +787,24 @@ define([ return value === $(param).val(); }, $.validator.messages.equalTo + ], + 'validate-file-type': [ + function (name, types) { + var extension = name.split('.').pop(); + + if (types && typeof types === 'string') { + types = types.split(' '); + } + + return !types || !types.length || ~types.indexOf(extension); + }, + $.mage.__('We don\'t recognize or support this file extension type.') + ], + 'validate-max-size': [ + function (size, maxSize) { + return maxSize === false || size < maxSize; + }, + $.mage.__('File you are trying to upload exceeds maximum file size limit.') ] }; }); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/preview.html b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/preview.html new file mode 100644 index 0000000000000..6de82dd4d8821 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/preview.html @@ -0,0 +1,59 @@ + +
+
+ + + + +
+ +
+
+ +
+
+ + x + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html new file mode 100644 index 0000000000000..d2ab54231f734 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html @@ -0,0 +1,81 @@ + +
+ + + + +
+
+
+ + + + + +
+
+ +
+ + +
+
+ + + +
+ +
+ + + + + + + + + +
+
+
\ No newline at end of file diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_components.less b/app/design/adminhtml/Magento/backend/web/css/source/_components.less index 53f89a1fc3ca8..d13cdececaab4 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_components.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_components.less @@ -14,3 +14,4 @@ @import 'components/_modals.less'; @import 'components/_modals_extend.less'; @import 'components/_file-insertion.less'; +@import 'components/_file-uploader.less'; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less new file mode 100644 index 0000000000000..584b2f00b48ae --- /dev/null +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less @@ -0,0 +1,290 @@ +// /** +// * Copyright © 2016 Magento. All rights reserved. +// * See COPYING.txt for license details. +// */ + +// +// Components -> Single File Uploader +// _____________________________________________ + +// +// Variables +// --------------------------------------------- + +@file-uploader-preview__border-color: @color-lighter-grayish; +@file-uploader-preview__background-color: @color-white; +@file-uploader-preview-focus__color: @color-blue2; + +@file-uploader-delete-icon__color: @color-brownie; +@file-uploader-delete-icon__hover__color: @color-brownie-vanilla; +@file-uploader-delete-icon__font-size: 2rem; + +@file-uploader-muted-text__color: @color-gray62; + +@file-uploader-preview__width: 150px; +@file-uploader-preview__height: @file-uploader-preview__width; +@file-uploader-preview__opacity: .7; + +@file-uploader-spinner-dimensions: 15px; + +@file-uploader-dragover__background: @color-gray83; +@file-uploader-dragover-focus__color: @color-blue2; + +// Grid uploader + +@data-grid-file-uploader-image__size: 5rem; +@data-grid-file-uploader-image__z-index: 1; + +@data-grid-file-uploader-menu-button__width: 2rem; + +@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; +@data-grid-file-uploader-upload-icon__line-height: 48px; + +@data-grid-file-uploader-wrapper__size: @data-grid-file-uploader-image__size + 2rem; + +// +// Single file uploader +// --------------------------------------------- + +.file-uploader-area { + margin-bottom: @indent__xs; + position: relative; + + input[type='file'] { + cursor: pointer; + opacity: 0; + overflow: hidden; + position: absolute; + width: 0; + + &:focus { + + .file-uploader-button { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } + } +} + +.file-uploader-button { + cursor: pointer; + display: inline-block; + + &._is-dragover { + background: @file-uploader-dragover__background; + border: 1px solid @file-uploader-preview-focus__color; + } +} + +.file-uploader-spinner { + background-image: url('@{baseDir}images/loader-1.gif'); + background-position: 50%; + background-repeat: no-repeat; + background-size: @file-uploader-spinner-dimensions; + display: none; + height: 30px; + margin-left: @indent__s; + vertical-align: top; + width: @file-uploader-spinner-dimensions; +} + +.file-uploader-preview { + background: @file-uploader-preview__background-color; + border: 1px solid @file-uploader-preview__border-color; + box-sizing: border-box; + cursor: pointer; + height: @file-uploader-preview__height; + line-height: 1; + margin-bottom: @indent__s; + overflow: hidden; + position: relative; + width: @file-uploader-preview__width; + + .action-remove { + &:extend(.abs-action-reset all); + .lib-icon-font ( + @icon-delete__content, + @_icon-font: @icons-admin__font-name, + @_icon-font-size: @file-uploader-delete-icon__font-size, + @_icon-font-color: @file-uploader-delete-icon__color, + @_icon-font-color-hover: @file-uploader-delete-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + bottom: 4px; + cursor: pointer; + display: block; + height: 27px; + left: 6px; + position: absolute; + text-decoration: none; + width: 25px; + z-index: 2; + } + + &:hover { + .preview-image { + opacity: @file-uploader-preview__opacity; + } + } + + .preview-image { + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: 1; + } +} + +.file-uploader { + &._loading { + .file-uploader-spinner { + display: inline-block; + } + } + + .admin__field-note, + .admin__field-error { + margin-bottom: @indent__s; + } + + .file-uploader-filename { + word-break: break-all; + + &:first-child { + margin-bottom: @indent__s; + } + } + + .file-uploader-meta { + color: @file-uploader-muted-text__color; + } + + .admin__field-fallback-reset { + margin-left: @indent__s; + } + + ._keyfocus & .action-remove { + &:focus { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } +} + +// +// Grid image uploader +// --------------------------------------------- + +.data-grid-file-uploader { + min-width: @data-grid-file-uploader-wrapper__size; + + &._loading { + .file-uploader-spinner { + display: block; + } + + .file-uploader-button { + &:before { + display: none; + } + } + } + + .file-uploader-image { + background: transparent; + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: @data-grid-file-uploader-image__z-index; + + + .file-uploader-area { + .file-uploader-button { + &:before { + display: none; + } + } + } + } + + .file-uploader-area { + z-index: @data-grid-file-uploader-image__z-index + 1; + } + + .file-uploader-spinner { + height: 100%; + margin: 0; + position: absolute; + top: 0; + width: 100%; + } + + .file-uploader-button { + display: block; + height: @data-grid-file-uploader-upload-icon__line-height; + text-align: center; + + .lib-icon-font ( + @icon-plus__content, + @_icon-font: @icons-admin__font-name, + @_icon-font-size: 1.3rem, + @_icon-font-line-height: @data-grid-file-uploader-upload-icon__line-height, + @_icon-font-color: @data-grid-file-uploader-upload-icon__color, + @_icon-font-color-hover: @data-grid-file-uploader-upload-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + } + + .action-select-wrap { + float: left; + + .action-select { + border: 1px solid @file-uploader-preview__border-color; + display: block; + height: @data-grid-file-uploader-image__size; + margin-left: -1px; + padding: 0; + width: @data-grid-file-uploader-menu-button__width; + + &:after { + border-color: @data-grid-file-uploader-upload-icon__color transparent transparent transparent; + left: 50%; + margin: 0 0 0 -5px; + } + + &:hover { + &:after { + border-color: @data-grid-file-uploader-upload-icon__hover__color transparent transparent transparent; + } + } + + > span { + display: none; + } + } + + .action-menu { + left: 4rem; + right: auto; + z-index: @data-grid-file-uploader-image__z-index + 1; + } + } +} + +.data-grid-file-uploader-inner { + border: 1px solid @file-uploader-preview__border-color; + float: left; + height: @data-grid-file-uploader-image__size; + position: relative; + width: @data-grid-file-uploader-image__size; +} \ No newline at end of file From a562accf1ef076377af0356c3d25131c130a7ce0 Mon Sep 17 00:00:00 2001 From: Volodymyr Zaets Date: Thu, 20 Oct 2016 18:27:42 +0300 Subject: [PATCH 33/48] MAGETWO-56171: [Backport] File added via customer file (attachement) attribute is not uploaded / found --- .../Ui/view/base/web/js/form/element/file-uploader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 7acd30bfe6c06..12637a0cc8938 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -16,8 +16,8 @@ define([ /** * Constructor to creating valid validation format * - * @param {(String|Object)} rule - One or many validation rules. - * @param {Boolean} passed. + * @param {String|Object} rule - One or many validation rules. + * @param {Boolean} passed * @param {String} msg - ErrorMessage */ var ValidationResult = function (rule, passed, msg) { @@ -414,4 +414,4 @@ define([ this.isDifferedFromDefault(!_.isEqual(value, this.default)); } }); -}); \ No newline at end of file +}); From 9e952e1753d9edf84c9e7122aa9382aa842b833d Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Sat, 22 Oct 2016 23:35:27 +0300 Subject: [PATCH 34/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Customer/Controller/Adminhtml/Index/Save.php | 16 +++++++++------- .../Unit/Controller/Adminhtml/Index/SaveTest.php | 7 +++++++ .../base/web/js/form/element/file-uploader.js | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 5f0f096a207d4..59ff3c159b189 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -82,6 +82,11 @@ protected function _extractData( $formData[$attributeCode] = isset($requestData[$attributeCode]) ? $requestData[$attributeCode] : false; } + $result = $metadataForm->compactData($formData); + + // Re-initialize additional attributes + $formData = array_replace($formData, $result); + // Unset unused attributes $formAttributes = $metadataForm->getAttributes(); foreach ($formAttributes as $attribute) { @@ -94,12 +99,7 @@ protected function _extractData( } } - $result = $metadataForm->compactData($formData); - - // Re-initialize additional attributes - $result = array_merge($result, array_diff_key($formData, $result)); - - return $result; + return $formData; } /** @@ -225,7 +225,9 @@ public function execute() ['customer' => $customer, 'request' => $this->getRequest()] ); $customer->setAddresses($addresses); - $customer->setStoreId($customerData['sendemail_store_id']); + if (isset($customerData['sendemail_store_id'])) { + $customer->setStoreId($customerData['sendemail_store_id']); + } // Save customer if ($customerId) { diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 7920c88b868e1..d450feaaf7020 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -300,6 +300,7 @@ public function testExecuteWithExistentCustomer() $dataToCompact = [ 'entity_id' => $customerId, 'code' => 'value', + 'coolness' => false, 'disable_auto_group_change' => 'false', CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, @@ -320,6 +321,7 @@ public function testExecuteWithExistentCustomer() 'default_billing' => 'true', 'default_shipping' => 'true', 'code' => 'value', + 'coolness' => false, 'region' => 'region', 'region_id' => 'region_id', ]; @@ -619,6 +621,7 @@ public function testExecuteWithNewCustomer() 'disable_auto_group_change' => 'false', ]; $dataToCompact = [ + 'coolness' => false, 'disable_auto_group_change' => 'false', CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, @@ -639,6 +642,7 @@ public function testExecuteWithNewCustomer() 'default_billing' => 'false', 'default_shipping' => 'false', 'code' => 'value', + 'coolness' => false, 'region' => 'region', 'region_id' => 'region_id', ]; @@ -894,6 +898,7 @@ public function testExecuteWithNewCustomerAndValidationException() 'disable_auto_group_change' => 'false', ]; $dataToCompact = [ + 'coolness' => false, 'disable_auto_group_change' => 'false', CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, @@ -1045,6 +1050,7 @@ public function testExecuteWithNewCustomerAndLocalizedException() 'disable_auto_group_change' => 'false', ]; $dataToCompact = [ + 'coolness' => false, 'disable_auto_group_change' => 'false', CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, @@ -1196,6 +1202,7 @@ public function testExecuteWithNewCustomerAndException() 'disable_auto_group_change' => 'false', ]; $dataToCompact = [ + 'coolness' => false, 'disable_auto_group_change' => 'false', CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 12637a0cc8938..50949811c3f14 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -23,7 +23,7 @@ define([ var ValidationResult = function (rule, passed, msg) { this.rule = rule || ''; this.passed = passed || false; - this.msg = msg || ''; + this.message = msg || ''; }; return Element.extend({ From e97b5914a61d99375c2212a692bc8f860d8b78f6 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 14:15:28 +0300 Subject: [PATCH 35/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 --- .../Model/Export/Product.php | 23 +++- .../Model/Import/Product.php | 123 ++++++++++++++++-- .../Import/Product/Type/AbstractType.php | 30 +++-- .../Model/Import/Product/Validator.php | 48 ++++--- .../Block/Adminhtml/Export/Edit/Form.php | 10 ++ .../Block/Adminhtml/Import/Edit/Form.php | 10 ++ .../Magento/ImportExport/Model/Export.php | 5 + .../Magento/ImportExport/Model/Import.php | 5 + .../templates/export/form/before.phtml | 3 + .../Catalog/_files/multiselect_attribute.php | 2 + ...select_attribute_with_incorrect_values.php | 55 ++++++++ .../Model/Export/ProductTest.php | 30 +++++ .../Model/Import/ProductTest.php | 51 ++++++++ ...s_to_import_with_additional_attributes.csv | 3 + .../Block/Adminhtml/Export/Edit/FormTest.php | 6 +- .../Controller/Adminhtml/ExportTest.php | 2 +- 16 files changed, 362 insertions(+), 44 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_incorrect_values.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_additional_attributes.csv diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index ab3d6e42789f9..b2a3027fbafbb 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -894,7 +894,7 @@ protected function collectRawData() if (is_scalar($attrValue)) { if (!in_array($fieldName, $this->_getExportMainAttrCodes())) { $additionalAttributes[$fieldName] = $fieldName . - ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $attrValue; + ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $this->wrapValue($attrValue); } $data[$itemId][$storeId][$fieldName] = $attrValue; } @@ -904,7 +904,7 @@ protected function collectRawData() $additionalAttributes[$code] = $fieldName . ImportProduct::PAIR_NAME_VALUE_SEPARATOR . implode( ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $this->collectedMultiselectsData[$storeId][$itemId][$code] + $this->wrapValue($this->collectedMultiselectsData[$storeId][$productLinkId][$code]) ); } } @@ -933,6 +933,25 @@ protected function collectRawData() return $data; } + /** + * Wrap values with double quotes if "Fields Enclosure" option is enabled + * + * @param string|array $value + * @return string|array + */ + private function wrapValue($value) + { + if (!empty($this->_parameters[\Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE])) { + $wrap = function ($value) { + return sprintf('"%s"', str_replace('"', '""', $value)); + }; + + $value = is_array($value) ? array_map($wrap, $value) : $wrap($value); + } + + return $value; + } + /** * @return array */ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 39b3c6ce471c6..56db80eb41aa6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -605,6 +605,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** @var array */ protected $rowNumbers = []; + /** + * Escaped separator value for regular expression. + * The value is based on PSEUDO_MULTI_LINE_SEPARATOR constant. + * @var string + */ + private $multiLineSeparatorForRegexp; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -2319,20 +2326,114 @@ private function _parseAdditionalAttributes($rowData) if (empty($rowData['additional_attributes'])) { return $rowData; } + $rowData = array_merge($rowData, $this->parseAdditionalAttributes($rowData['additional_attributes'])); - $attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $rowData['additional_attributes']); - foreach ($attributeNameValuePairs as $attributeNameValuePair) { - $separatorPosition = strpos($attributeNameValuePair, self::PAIR_NAME_VALUE_SEPARATOR); - if ($separatorPosition !== false) { - $key = substr($attributeNameValuePair, 0, $separatorPosition); - $value = substr( - $attributeNameValuePair, - $separatorPosition + strlen(self::PAIR_NAME_VALUE_SEPARATOR) - ); - $rowData[$key] = $value === false ? '' : $value; + return $rowData; + } + + /** + * Retrieves additional attributes in format: + * [ + * code1 => value1, + * code2 => value2, + * ... + * codeN => valueN + * ] + * + * @param string $additionalAttributes Attributes data that will be parsed + * @return array + */ + private function parseAdditionalAttributes($additionalAttributes) + { + return empty($this->_parameters[Import::FIELDS_ENCLOSURE]) + ? $this->parseAttributesWithoutWrappedValues($additionalAttributes) + : $this->parseAttributesWithWrappedValues($additionalAttributes); + } + + /** + * Parses data and returns attributes in format: + * [ + * code1 => value1, + * code2 => value2, + * ... + * codeN => valueN + * ] + * + * @param string $attributesData Attributes data that will be parsed. It keeps data in format: + * code=value,code2=value2...,codeN=valueN + * @return array + */ + private function parseAttributesWithoutWrappedValues($attributesData) + { + $attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $attributesData); + $preparedAttributes = []; + $code = ''; + foreach ($attributeNameValuePairs as $attributeData) { + //process case when attribute has ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR inside its value + if (strpos($attributeData, self::PAIR_NAME_VALUE_SEPARATOR) === false) { + if (!$code) { + continue; + } + $preparedAttributes[$code] .= $this->getMultipleValueSeparator() . $attributeData; + continue; } + list($code, $value) = explode(self::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2); + $preparedAttributes[$code] = $value; } - return $rowData; + + return $preparedAttributes; + } + + /** + * Parses data and returns attributes in format: + * [ + * code1 => value1, + * code2 => value2, + * ... + * codeN => valueN + * ] + * All values have unescaped data except mupliselect attributes, + * they should be parsed in additional method - parseMultiselectValues() + * + * @param string $attributesData Attributes data that will be parsed. It keeps data in format: + * code="value",code2="value2"...,codeN="valueN" + * where every value is wrapped in double quotes. Double quotes as part of value should be duplicated. + * E.g. attribute with code 'attr_code' has value 'my"value'. This data should be stored as attr_code="my""value" + * + * @return array + */ + private function parseAttributesWithWrappedValues($attributesData) + { + $attributes = []; + preg_match_all('~((?:[a-z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~', + $attributesData, + $matches + ); + foreach ($matches[1] as $i => $attributeCode) { + $attribute = $this->retrieveAttributeByCode($attributeCode); + $value = 'multiselect' != $attribute->getFrontendInput() + ? str_replace('""', '"', $matches[2][$i]) + : '"' . $matches[2][$i] . '"'; + $attributes[$attributeCode] = $value; + } + + return $attributes; + } + + + /** + * Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression + * + * @return string + */ + private function getMultiLineSeparatorForRegexp() + { + if (!$this->multiLineSeparatorForRegexp) { + $this->multiLineSeparatorForRegexp = in_array(self::PSEUDO_MULTI_LINE_SEPARATOR, str_split('[\^$.|?*+(){}')) + ? '\\' . self::PSEUDO_MULTI_LINE_SEPARATOR + : self::PSEUDO_MULTI_LINE_SEPARATOR; + } + return $this->multiLineSeparatorForRegexp; } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 80dead2a2ff75..7a74e79d77b87 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -475,23 +475,25 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe $resultAttrs = []; foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) { - if (!$attrParams['is_static']) { - if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { - $resultAttrs[$attrCode] = in_array($attrParams['type'], ['select', 'boolean']) - ? $attrParams['options'][strtolower($rowData[$attrCode])] - : $rowData[$attrCode]; - if ('multiselect' == $attrParams['type']) { - $resultAttrs[$attrCode] = []; - foreach (explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]) as $value) { - $resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)]; - } - $resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]); + if ($attrParams['is_static']) { + continue; + } + if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { + if (in_array($attrParams['type'], ['select', 'boolean'])) { + $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; + } elseif ('multiselect' == $attrParams['type']) { + $resultAttrs[$attrCode] = []; + foreach ($this->_entityModel->parseMultiselectValues($rowData[$attrCode]) as $value) { + $resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)]; } - } elseif (array_key_exists($attrCode, $rowData)) { + $resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]); + } else { $resultAttrs[$attrCode] = $rowData[$attrCode]; - } elseif ($withDefaultValue && null !== $attrParams['default_value']) { - $resultAttrs[$attrCode] = $attrParams['default_value']; } + } elseif (array_key_exists($attrCode, $rowData)) { + $resultAttrs[$attrCode] = $rowData[$attrCode]; + } elseif ($withDefaultValue && null !== $attrParams['default_value']) { + $resultAttrs[$attrCode] = $attrParams['default_value']; } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 3ad455a33afab..55b0781951b8d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -66,6 +66,32 @@ protected function textValidation($attrCode, $type) return $valid; } + /** + * Check if value is valid attribute option + * + * @param string $attrCode + * @param array $possibleOptions + * @param string $value + * @return bool + */ + private function validateOption($attrCode, $possibleOptions, $value) + { + if (!isset($possibleOptions[strtolower($value)])) { + $this->_addMessages( + [ + sprintf( + $this->context->retrieveMessageTemplate( + RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION + ), + $attrCode + ) + ] + ); + return false; + } + return true; + } + /** * @param mixed $attrCode * @param string $type @@ -161,23 +187,15 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) break; case 'select': case 'boolean': + $valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]); + break; case 'multiselect': - $values = explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]); - $valid = true; + $values = $this->context->parseMultiselectValues($rowData[$attrCode]); foreach ($values as $value) { - $valid = $valid && isset($attrParams['options'][strtolower($value)]); - } - if (!$valid) { - $this->_addMessages( - [ - sprintf( - $this->context->retrieveMessageTemplate( - RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION - ), - $attrCode - ) - ] - ); + $valid = $this->validateOption($attrCode, $attrParams['options'], $value); + if (!$valid) { + break; + } } break; case 'datetime': diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Edit/Form.php index a56642d1dda33..fec1684f39f36 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Edit/Form.php @@ -86,6 +86,16 @@ protected function _prepareForm() 'values' => $this->_formatFactory->create()->toOptionArray() ] ); + $fieldset->addField( + \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE, + 'checkbox', + [ + 'name' => \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE, + 'label' => __('Fields Enclosure'), + 'title' => __('Fields Enclosure'), + 'value' => 1, + ] + ); $form->setUseContainer(true); $this->setForm($form); diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index 1511f58d4b039..435a663923306 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -174,6 +174,16 @@ protected function _prepareForm() 'value' => Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, ] ); + $fieldsets[$behaviorCode]->addField( + $behaviorCode . \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE, + 'checkbox', + [ + 'name' => \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE, + 'label' => __('Fields enclosure'), + 'title' => __('Fields enclosure'), + 'value' => 1, + ] + ); } // fieldset for file uploading diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index b11f896bee02a..7fcf9f9316377 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -20,6 +20,11 @@ class Export extends \Magento\ImportExport\Model\AbstractModel const FILTER_ELEMENT_SKIP = 'skip_attr'; + /** + * Allow multiple values wrapping in double quotes for additional attributes. + */ + const FIELDS_ENCLOSURE = 'fields_enclosure'; + /** * Filter fields types. */ diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 207cb96eaa99d..dda970da59904 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -78,6 +78,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ const FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR = '_import_multiple_value_separator'; + /** + * Allow multiple values wrapping in double quotes for additional attributes. + */ + const FIELDS_ENCLOSURE = 'fields_enclosure'; + /**#@-*/ /** diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml index b8acac6a1ab64..ce4f61cef4feb 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml @@ -80,6 +80,9 @@ require([ var oldAction = form.action; var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + $F('entity') + '/file_format/' + $F('file_format'); + if ($F('fields_enclosure')) { + url += '/fields_enclosure/' + $F('fields_enclosure'); + } form.action = url; form.submit(); form.action = oldAction; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute.php index d0fe9b7cc3a30..2f3dd6fe200c1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute.php @@ -38,11 +38,13 @@ 'option_1' => ['Option 1'], 'option_2' => ['Option 2'], 'option_3' => ['Option 3'], + 'option_4' => ['Option 4 "!@#$%^&*'] ], 'order' => [ 'option_1' => 1, 'option_2' => 2, 'option_3' => 3, + 'option_4' => 4, ], ], ] diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_incorrect_values.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_incorrect_values.php new file mode 100644 index 0000000000000..1ab2f9ef65bff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_incorrect_values.php @@ -0,0 +1,55 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class +); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); +$attribute->setData( + [ + 'attribute_code' => 'multiselect_attribute', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'multiselect', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Multiselect Attribute'], + 'backend_type' => 'varchar', + 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, + 'option' => [ + 'value' => [ + 'option_1' => ['Opt|,=ion 1'], + 'option_2' => ['Opt||,ion 2'], + 'option_3' => ['Option 3 "!@#$%^&*, "|"'] + ], + 'order' => [ + 'option_1' => 1, + 'option_2' => 2, + 'option_3' => 3, + ], + ], + ] +); +$attribute->save(); + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 5c4cda2616ec1..2f0bae3bfe0c9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -15,6 +15,11 @@ class ProductTest extends \PHPUnit_Framework_TestCase */ protected $_model; + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $objectManager; + /** * Stock item attributes which must be exported * @@ -47,6 +52,7 @@ protected function setUp() { parent::setUp(); + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( 'Magento\CatalogImportExport\Model\Export\Product' ); @@ -171,4 +177,28 @@ public function testExceptionInGetExportData() $data = $model->setWriter($exportAdapter)->export(); $this->assertEmpty($data); } + + /** + * Verify if fields wrapping works correct when "Fields Enclosure" option enabled + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + */ + public function testExportWithFieldsEnclosure() + { + $this->_model->setParameters([ + \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE => 1 + ]); + + $this->_model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->_model->export(); + + $this->assertContains('""Option 2""', $exportData); + $this->assertContains('""Option 3""', $exportData); + $this->assertContains('""Option 4 """"!@#$%^&*""', $exportData); + $this->assertContains('text_attribute=""!@#$%^&*()_+1234567890-=|\:;""""\'<,>.?/', $exportData); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 3fd3419b5554e..2cdb52f3b16f6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1005,4 +1005,55 @@ function(ProductInterface $item) { array_values($actualProductSkus) ); } + + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute_with_incorrect_values.php + * @magentoDataFixture Magento/Catalog/_files/product_text_attribute.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testProductWithWrappedAdditionalAttributes() + { + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_with_additional_attributes.csv', + 'directory' => $directory + ] + ); + $errors = $this->_model->setParameters( + [ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product', + \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE => 1 + ] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + + $this->_model->importData(); + + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + + /** @var \Magento\Eav\Api\AttributeOptionManagementInterface $multiselectOptions */ + $multiselectOptions = $this->objectManager->get(\Magento\Eav\Api\AttributeOptionManagementInterface::class) + ->getItems(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_attribute'); + + $product1 = $productRepository->get('simple1'); + $this->assertEquals('\'", =|', $product1->getData('text_attribute')); + $this->assertEquals(implode(',', [$multiselectOptions[3]->getValue(), $multiselectOptions[2]->getValue()]), + $product1->getData('multiselect_attribute')); + + $product2 = $productRepository->get('simple2'); + $this->assertEquals('', $product2->getData('text_attribute')); + $this->assertEquals(implode(',', [$multiselectOptions[1]->getValue(), $multiselectOptions[2]->getValue()]), + $product2->getData('multiselect_attribute')); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_additional_attributes.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_additional_attributes.csv new file mode 100644 index 0000000000000..eb882be4c6bb1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_additional_attributes.csv @@ -0,0 +1,3 @@ +sku,product_type,name,price,attribute_set_code,categories,additional_attributes +simple1,simple,"simple 1",25,Default,"Default Category/Category 1","text_attribute=""'"""", =|"",multiselect_attribute=""Option 3 """"!@#$%^&*, """"|""""""|""Opt||,ion 2""" +simple2,simple,"simple 2",34,Default,"Default Category/Category 1","multiselect_attribute=""Opt|,=ion 1""|""Opt||,ion 2"",text_attribute=""""" diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Block/Adminhtml/Export/Edit/FormTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Block/Adminhtml/Export/Edit/FormTest.php index fe63890a87915..143709c64a431 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Block/Adminhtml/Export/Edit/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Block/Adminhtml/Export/Edit/FormTest.php @@ -30,7 +30,11 @@ class FormTest extends \PHPUnit_Framework_TestCase * * @var array */ - protected $_expectedFields = ['base_fieldset' => ['entity' => 'entity', 'file_format' => 'file_format']]; + protected $_expectedFields = ['base_fieldset' => [ + 'entity' => 'entity', + 'file_format' => 'file_format', + 'fields_enclosure' => 'fields_enclosure' + ]]; protected function setUp() { diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ExportTest.php index 9decf617d50ae..de9379d1494d3 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ExportTest.php @@ -84,6 +84,6 @@ public function testIndexAction() $body = $this->getResponse()->getBody(); $this->assertSelectCount('fieldset#base_fieldset', 1, $body); - $this->assertSelectCount('fieldset#base_fieldset div.field', 2, $body); + $this->assertSelectCount('fieldset#base_fieldset div.field', 3, $body); } } From ed355ceae53ee7d6774dceee946e75083fdb3aeb Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 14:21:36 +0300 Subject: [PATCH 36/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 --- app/code/Magento/CatalogImportExport/Model/Export/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index b2a3027fbafbb..cf0a61152bf88 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -904,7 +904,7 @@ protected function collectRawData() $additionalAttributes[$code] = $fieldName . ImportProduct::PAIR_NAME_VALUE_SEPARATOR . implode( ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $this->wrapValue($this->collectedMultiselectsData[$storeId][$productLinkId][$code]) + $this->wrapValue($this->collectedMultiselectsData[$storeId][$itemId][$code]) ); } } From ec74cc92da3fe754f470ff95e008c86688c2a972 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 14:29:00 +0300 Subject: [PATCH 37/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 --- .../Model/Import/Product.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 56db80eb41aa6..d6164da5d9811 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -2327,7 +2327,6 @@ private function _parseAdditionalAttributes($rowData) return $rowData; } $rowData = array_merge($rowData, $this->parseAdditionalAttributes($rowData['additional_attributes'])); - return $rowData; } @@ -2380,7 +2379,6 @@ private function parseAttributesWithoutWrappedValues($attributesData) list($code, $value) = explode(self::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2); $preparedAttributes[$code] = $value; } - return $preparedAttributes; } @@ -2416,10 +2414,27 @@ private function parseAttributesWithWrappedValues($attributesData) : '"' . $matches[2][$i] . '"'; $attributes[$attributeCode] = $value; } - return $attributes; } + /** + * Parse values of multiselect attributes depends on "Fields Enclosure" parameter + * + * @param string $values + * @return array + */ + public function parseMultiselectValues($values) + { + if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) { + return explode(self::PSEUDO_MULTI_LINE_SEPARATOR, $values); + } + if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { + return $values = array_map(function ($value) { + return str_replace('""', '"', $value); + }, $matches[1]); + } + return [$values]; + } /** * Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression From 46c88f24e371ae26667f0755373f78ee08964d2d Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 14:59:58 +0300 Subject: [PATCH 38/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 Add fixture --- .../Catalog/_files/product_text_attribute.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute.php new file mode 100644 index 0000000000000..d727745e7dd2c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute.php @@ -0,0 +1,38 @@ +create(\Magento\Catalog\Setup\CategorySetup::class); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +$attribute->setData( + [ + 'attribute_code' => 'text_attribute', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'textarea', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Text Attribute'], + 'backend_type' => 'text', + ] +); +$attribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); From 20acd553a23fe9d78131491ea9b119b5f2877da0 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 15:16:31 +0300 Subject: [PATCH 39/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 --- .../Catalog/_files/products_with_multiselect_attribute.php | 2 +- .../Magento/CatalogImportExport/_files/product_export_data.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php index 3ccc3e58b64f1..dc70dc3719881 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php @@ -59,7 +59,7 @@ )->setVisibility( \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH )->setMultiselectAttribute( - [$optionIds[1], $optionIds[2]] + [$optionIds[1], $optionIds[2], $optionIds[3]] )->setStatus( \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED )->setStockData( diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php index b99c1c2dca7d7..abe48cefd9f6a 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php @@ -7,6 +7,7 @@ require dirname(dirname(__DIR__)) . '/Catalog/_files/category.php'; require dirname(dirname(__DIR__)) . '/Store/_files/second_store.php'; require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute.php'; +require dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute.php'; $productModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); @@ -37,6 +38,8 @@ 'simple' )->setPrice( 10 +)->addData( + ['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/'] )->setTierPrice( [0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]] )->setVisibility( From f19ca9ea1bb41ad29d557716840d1a058e181f95 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Mon, 24 Oct 2016 17:49:36 +0300 Subject: [PATCH 40/48] Merge branches '2.0-develop' and 'MAGETWO-57783' of github.com:magento-firedrakes/magento2ce into MAGETWO-57783 # Conflicts: # composer.json # composer.lock --- composer.json | 1 + composer.lock | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index cd00b1329740c..932f3282b29d1 100644 --- a/composer.json +++ b/composer.json @@ -151,6 +151,7 @@ "magento/module-rss": "100.0.5", "magento/module-rule": "100.0.6", "magento/module-sales": "100.0.11", + "magento/module-sales-inventory": "100.0.0", "magento/module-sales-rule": "100.0.7", "magento/module-sales-sequence": "100.0.5", "magento/module-sample-data": "100.0.5", diff --git a/composer.lock b/composer.lock index 8bb0a296b16f9..8de272b424ce9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "78fb0de58c5d6be500c20bc8c0ce4a44", - "content-hash": "c2139dc47051ee9a26c32330bd0471f8", + "hash": "c5518cc3f7da327f83e9fb5f0b9560eb", + "content-hash": "631785f3626bb2df193c8142eabae9e1", "packages": [ { "name": "braintree/braintree_php", From 45ba9ff3ac4f298a3535d3c408109f456561c0d9 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Tue, 25 Oct 2016 14:54:51 +0300 Subject: [PATCH 41/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../base/web/js/form/element/file-uploader.js | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 50949811c3f14..9bd0d9a49d9dc 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -41,9 +41,6 @@ define([ formData: { 'form_key': window.FORM_KEY } - }, - tracks: { - isLoading: true } }, @@ -71,6 +68,19 @@ define([ return this; }, + /** + * Initializes observable properties of instance + * + * @returns {Abstract} Chainable. + */ + initObservable: function () { + this._super(); + + this.observe('isLoading'); + + return this; + }, + /** * Defines initial value of the instance. * @@ -362,14 +372,14 @@ define([ * Load start event handler. */ onLoadingStart: function () { - this.isLoading = true; + this.isLoading(true); }, /** * Load stop event handler. */ onLoadingStop: function () { - this.isLoading = false; + this.isLoading(false); }, /** From ab7693b75525f85ea02e322494d625102a6a7f64 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Tue, 25 Oct 2016 15:04:50 +0300 Subject: [PATCH 42/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Fix major svc changes --- .../Sales/Model/Order/PaymentAdapter.php | 22 ----------- .../Model/Order/PaymentAdapterInterface.php | 12 ------ .../Sales/Model/Order/RefundAdapter.php | 39 +++++++++++++++++++ .../Model/Order/RefundAdapterInterface.php | 26 +++++++++++++ .../Magento/Sales/Model/RefundInvoice.php | 14 +++---- app/code/Magento/Sales/Model/RefundOrder.php | 14 +++---- .../Sales/Model/Service/CreditmemoService.php | 18 ++++----- .../Test/Unit/Model/RefundInvoiceTest.php | 14 +++---- .../Sales/Test/Unit/Model/RefundOrderTest.php | 14 +++---- .../Model/Service/CreditmemoServiceTest.php | 8 ++-- app/code/Magento/Sales/etc/di.xml | 1 + 11 files changed, 107 insertions(+), 75 deletions(-) create mode 100644 app/code/Magento/Sales/Model/Order/RefundAdapter.php create mode 100644 app/code/Magento/Sales/Model/Order/RefundAdapterInterface.php diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php index d176ce0566bb3..8379ae062ca89 100644 --- a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php @@ -7,45 +7,23 @@ /** * Payment adapter. - * - * @api */ class PaymentAdapter implements PaymentAdapterInterface { - /** - * @var \Magento\Sales\Model\Order\Creditmemo\RefundOperation - */ - private $refundOperation; - /** * @var \Magento\Sales\Model\Order\Invoice\PayOperation */ private $payOperation; /** - * PaymentAdapter constructor. - * @param \Magento\Sales\Model\Order\Creditmemo\RefundOperation $refundOperation * @param \Magento\Sales\Model\Order\Invoice\PayOperation $payOperation */ public function __construct( - \Magento\Sales\Model\Order\Creditmemo\RefundOperation $refundOperation, \Magento\Sales\Model\Order\Invoice\PayOperation $payOperation ) { - $this->refundOperation = $refundOperation; $this->payOperation = $payOperation; } - /** - * {@inheritdoc} - */ - public function refund( - \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, - \Magento\Sales\Api\Data\OrderInterface $order, - $isOnline = false - ) { - return $this->refundOperation->execute($creditmemo, $order, $isOnline); - } - /** * {@inheritdoc} */ diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php index 3636bc2592f3b..0e4b193169da8 100644 --- a/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php @@ -23,16 +23,4 @@ interface PaymentAdapterInterface * @return OrderInterface */ public function pay(OrderInterface $order, InvoiceInterface $invoice, $capture); - - /** - * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo - * @param \Magento\Sales\Api\Data\OrderInterface $order - * @param bool $isOnline - * @return \Magento\Sales\Api\Data\OrderInterface - */ - public function refund( - \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, - \Magento\Sales\Api\Data\OrderInterface $order, - $isOnline = false - ); } diff --git a/app/code/Magento/Sales/Model/Order/RefundAdapter.php b/app/code/Magento/Sales/Model/Order/RefundAdapter.php new file mode 100644 index 0000000000000..a66b16f96dd7d --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/RefundAdapter.php @@ -0,0 +1,39 @@ +refundOperation = $refundOperation; + } + + /** + * {@inheritdoc} + */ + public function refund( + \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo, + \Magento\Sales\Api\Data\OrderInterface $order, + $isOnline = false + ) { + return $this->refundOperation->execute($creditmemo, $order, $isOnline); + } +} diff --git a/app/code/Magento/Sales/Model/Order/RefundAdapterInterface.php b/app/code/Magento/Sales/Model/Order/RefundAdapterInterface.php new file mode 100644 index 0000000000000..afcbb1c773d17 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/RefundAdapterInterface.php @@ -0,0 +1,26 @@ +invoiceRepository = $invoiceRepository; $this->validator = $validator; $this->creditmemoRepository = $creditmemoRepository; - $this->paymentAdapter = $paymentAdapter; + $this->refundAdapter = $refundAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; $this->notifier = $notifier; $this->config = $config; @@ -164,7 +164,7 @@ public function execute( try { $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); $order->setCustomerNoteNotify($notify); - $order = $this->paymentAdapter->refund($creditmemo, $order, $isOnline); + $order = $this->refundAdapter->refund($creditmemo, $order, $isOnline); $order->setState( $this->orderStateResolver->getStateForOrder($order, []) ); diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php index abd6e25416729..4bb66689b84f2 100644 --- a/app/code/Magento/Sales/Model/RefundOrder.php +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -13,7 +13,7 @@ use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\RefundAdapterInterface; use Magento\Sales\Model\Order\Validation\RefundOrderInterface as RefundOrderValidator; use Psr\Log\LoggerInterface; @@ -44,9 +44,9 @@ class RefundOrder implements RefundOrderInterface private $creditmemoRepository; /** - * @var PaymentAdapterInterface + * @var RefundAdapterInterface */ - private $paymentAdapter; + private $refundAdapter; /** * @var CreditmemoDocumentFactory @@ -80,7 +80,7 @@ class RefundOrder implements RefundOrderInterface * @param OrderStateResolverInterface $orderStateResolver * @param OrderRepositoryInterface $orderRepository * @param CreditmemoRepositoryInterface $creditmemoRepository - * @param PaymentAdapterInterface $paymentAdapter + * @param RefundAdapterInterface $refundAdapter * @param CreditmemoDocumentFactory $creditmemoDocumentFactory * @param RefundOrderValidator $validator * @param NotifierInterface $notifier @@ -93,7 +93,7 @@ public function __construct( OrderStateResolverInterface $orderStateResolver, OrderRepositoryInterface $orderRepository, CreditmemoRepositoryInterface $creditmemoRepository, - PaymentAdapterInterface $paymentAdapter, + RefundAdapterInterface $refundAdapter, CreditmemoDocumentFactory $creditmemoDocumentFactory, RefundOrderValidator $validator, NotifierInterface $notifier, @@ -104,7 +104,7 @@ public function __construct( $this->orderStateResolver = $orderStateResolver; $this->orderRepository = $orderRepository; $this->creditmemoRepository = $creditmemoRepository; - $this->paymentAdapter = $paymentAdapter; + $this->refundAdapter = $refundAdapter; $this->creditmemoDocumentFactory = $creditmemoDocumentFactory; $this->validator = $validator; $this->notifier = $notifier; @@ -150,7 +150,7 @@ public function execute( try { $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); $order->setCustomerNoteNotify($notify); - $order = $this->paymentAdapter->refund($creditmemo, $order); + $order = $this->refundAdapter->refund($creditmemo, $order); $order->setState( $this->orderStateResolver->getStateForOrder($order, []) ); diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index 4889ccd3750d5..f8a83bdafd1c1 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -53,9 +53,9 @@ class CreditmemoService implements \Magento\Sales\Api\CreditmemoManagementInterf private $resource; /** - * @var \Magento\Sales\Model\Order\PaymentAdapterInterface + * @var \Magento\Sales\Model\Order\RefundAdapterInterface */ - private $paymentAdapter; + private $refundAdapter; /** * @var \Magento\Sales\Api\OrderRepositoryInterface @@ -163,7 +163,7 @@ public function refund( $connection = $this->getResource()->getConnection('sales'); $connection->beginTransaction(); try { - $order = $this->getPaymentAdapter()->refund( + $order = $this->getRefundAdapter()->refund( $creditmemo, $creditmemo->getOrder(), !$offlineRequested @@ -219,17 +219,17 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface } /** - * @return \Magento\Sales\Model\Order\PaymentAdapterInterface + * @return \Magento\Sales\Model\Order\RefundAdapterInterface * * @deprecated */ - private function getPaymentAdapter() + private function getRefundAdapter() { - if ($this->paymentAdapter === null) { - $this->paymentAdapter = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Sales\Model\Order\PaymentAdapterInterface::class); + if ($this->refundAdapter === null) { + $this->refundAdapter = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\Order\RefundAdapterInterface::class); } - return $this->paymentAdapter; + return $this->refundAdapter; } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php index d249f039a140b..f30e76df5402d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -20,7 +20,7 @@ use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; -use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\RefundAdapterInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; use Magento\Sales\Model\Order\Validation\RefundInvoiceInterface; use Magento\Sales\Model\ValidatorResultInterface; @@ -55,9 +55,9 @@ class RefundInvoiceTest extends \PHPUnit_Framework_TestCase private $creditmemoDocumentFactoryMock; /** - * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RefundAdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $paymentAdapterMock; + private $refundAdapterMock; /** * @var OrderStateResolverInterface|\PHPUnit_Framework_MockObject_MockObject @@ -148,7 +148,7 @@ protected function setUp() $this->creditmemoDocumentFactoryMock = $this->getMockBuilder(CreditmemoDocumentFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + $this->refundAdapterMock = $this->getMockBuilder(RefundAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); $this->refundInvoiceValidatorMock = $this->getMockBuilder(RefundInvoiceInterface::class) @@ -212,7 +212,7 @@ protected function setUp() $this->invoiceRepositoryMock, $this->refundInvoiceValidatorMock, $this->creditmemoRepositoryMock, - $this->paymentAdapterMock, + $this->refundAdapterMock, $this->creditmemoDocumentFactoryMock, $this->notifierMock, $this->configMock, @@ -268,7 +268,7 @@ public function testOrderCreditmemo($invoiceId, $isOnline, $items, $notify, $app $hasMessages = false; $this->validationMessagesMock->expects($this->once()) ->method('hasMessages')->willReturn($hasMessages); - $this->paymentAdapterMock->expects($this->once()) + $this->refundAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willReturn($this->orderMock); @@ -440,7 +440,7 @@ public function testCouldNotCreditmemoException() ->method('hasMessages')->willReturn($hasMessages); $e = new \Exception(); - $this->paymentAdapterMock->expects($this->once()) + $this->refundAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willThrowException($e); diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index e06848452e712..f9afda783453e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -18,7 +18,7 @@ use Magento\Sales\Model\Order\CreditmemoDocumentFactory; use Magento\Sales\Model\Order\OrderStateResolverInterface; use Magento\Sales\Model\Order\Validation\RefundOrderInterface; -use Magento\Sales\Model\Order\PaymentAdapterInterface; +use Magento\Sales\Model\Order\RefundAdapterInterface; use Magento\Sales\Model\Order\Creditmemo\NotifierInterface; use Magento\Sales\Model\RefundOrder; use Magento\Sales\Model\ValidatorResultInterface; @@ -48,9 +48,9 @@ class RefundOrderTest extends \PHPUnit_Framework_TestCase private $creditmemoDocumentFactoryMock; /** - * @var PaymentAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RefundAdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $paymentAdapterMock; + private $refundAdapterMock; /** * @var OrderStateResolverInterface|\PHPUnit_Framework_MockObject_MockObject @@ -136,7 +136,7 @@ protected function setUp() $this->refundOrderValidatorMock = $this->getMockBuilder(RefundOrderInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + $this->refundAdapterMock = $this->getMockBuilder(RefundAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) @@ -182,7 +182,7 @@ protected function setUp() $this->orderStateResolverMock, $this->orderRepositoryMock, $this->creditmemoRepositoryMock, - $this->paymentAdapterMock, + $this->refundAdapterMock, $this->creditmemoDocumentFactoryMock, $this->refundOrderValidatorMock, $this->notifierMock, @@ -233,7 +233,7 @@ public function testOrderCreditmemo($orderId, $notify, $appendComment) $hasMessages = false; $this->validationMessagesMock->expects($this->once()) ->method('hasMessages')->willReturn($hasMessages); - $this->paymentAdapterMock->expects($this->once()) + $this->refundAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willReturn($this->orderMock); @@ -388,7 +388,7 @@ public function testCouldNotCreditmemoException() $this->validationMessagesMock->expects($this->once()) ->method('hasMessages')->willReturn($hasMessages); $e = new \Exception(); - $this->paymentAdapterMock->expects($this->once()) + $this->refundAdapterMock->expects($this->once()) ->method('refund') ->with($this->creditmemoMock, $this->orderMock) ->willThrowException($e); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php index c0593fede2648..a3538e0a751a5 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php @@ -202,13 +202,13 @@ public function testRefund() ->method('round') ->willReturnArgument(0); // Set payment adapter dependency - $paymentAdapterMock = $this->getMockBuilder(\Magento\Sales\Model\Order\PaymentAdapterInterface::class) + $refundAdapterMock = $this->getMockBuilder(\Magento\Sales\Model\Order\RefundAdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->setBackwardCompatibleProperty( $this->creditmemoService, - 'paymentAdapter', - $paymentAdapterMock + 'refundAdapter', + $refundAdapterMock ); // Set resource dependency $resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) @@ -233,7 +233,7 @@ public function testRefund() ->getMockForAbstractClass(); $resourceMock->expects($this->once())->method('getConnection')->with('sales')->willReturn($adapterMock); $adapterMock->expects($this->once())->method('beginTransaction'); - $paymentAdapterMock->expects($this->once()) + $refundAdapterMock->expects($this->once()) ->method('refund') ->with($creditMemoMock, $orderMock, false) ->willReturn($orderMock); diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 0e1ef5a1109a3..1be547fe0704e 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -75,6 +75,7 @@ + From 99b17bc04cfe00a4e59832ecf0e3b80951d12574 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Tue, 25 Oct 2016 15:24:58 +0300 Subject: [PATCH 43/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Fix major svc changes --- app/code/Magento/Sales/Model/Order/PaymentAdapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php index 8379ae062ca89..84eb0fa07553c 100644 --- a/app/code/Magento/Sales/Model/Order/PaymentAdapter.php +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapter.php @@ -7,6 +7,8 @@ /** * Payment adapter. + * + * @api */ class PaymentAdapter implements PaymentAdapterInterface { From f194f7682ab74f18c7875bc7999632014403ca63 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Tue, 25 Oct 2016 16:30:08 +0300 Subject: [PATCH 44/48] MAGETWO-57783: Create and Apply patch for backport to 2.0. Fix major svc changes --- .../Unit/Model/Order/PaymentAdapterTest.php | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php index 8d4daec7bf8d5..17fc2f5d6ed25 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php @@ -25,11 +25,6 @@ class PaymentAdapterTest extends \PHPUnit_Framework_TestCase */ private $creditmemoMock; - /** - * @var \Magento\Sales\Model\Order\Creditmemo\RefundOperation|\PHPUnit_Framework_MockObject_MockObject - */ - private $refundOperationMock; - /** * @var \Magento\Sales\Api\Data\InvoiceInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -49,11 +44,6 @@ protected function setUp() $this->creditmemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - - $this->refundOperationMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Creditmemo\RefundOperation::class) - ->disableOriginalConstructor() - ->getMock(); - $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -63,24 +53,10 @@ protected function setUp() ->getMock(); $this->subject = new \Magento\Sales\Model\Order\PaymentAdapter( - $this->refundOperationMock, $this->payOperationMock ); } - public function testRefund() - { - $isOnline = true; - $this->refundOperationMock->expects($this->once()) - ->method('execute') - ->with($this->creditmemoMock, $this->orderMock, $isOnline) - ->willReturn($this->orderMock); - $this->assertEquals( - $this->orderMock, - $this->subject->refund($this->creditmemoMock, $this->orderMock, $isOnline) - ); - } - public function testPay() { $isOnline = true; From 7d8c2e6e0ce2ff70d8d9d8b3fa61c3487826a5b6 Mon Sep 17 00:00:00 2001 From: Sergey Semenov Date: Wed, 26 Oct 2016 16:49:24 +0300 Subject: [PATCH 45/48] MAGETWO-56171: File added via customer file (attachement) attribute is not uploaded / found --- .../Magento/Customer/Model/FileProcessor.php | 24 ++++++++++++++ .../Test/Unit/Model/FileProcessorTest.php | 32 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index f521753d133c7..4a38406462f40 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Url\EncoderInterface; @@ -56,12 +57,18 @@ class FileProcessor */ private $allowedExtensions = []; + /** + * @var Mime + */ + private $mime; + /** * @param Filesystem $filesystem * @param UploaderFactory $uploaderFactory * @param UrlInterface $urlBuilder * @param EncoderInterface $urlEncoder * @param string $entityTypeCode + * @param Mime $mime * @param array $allowedExtensions */ public function __construct( @@ -70,6 +77,7 @@ public function __construct( UrlInterface $urlBuilder, EncoderInterface $urlEncoder, $entityTypeCode, + Mime $mime, array $allowedExtensions = [] ) { $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); @@ -77,6 +85,7 @@ public function __construct( $this->urlBuilder = $urlBuilder; $this->urlEncoder = $urlEncoder; $this->entityTypeCode = $entityTypeCode; + $this->mime = $mime; $this->allowedExtensions = $allowedExtensions; } @@ -110,6 +119,21 @@ public function getStat($fileName) return $result; } + /** + * Retrieve MIME type of requested file + * + * @param string $fileName + * @return string + */ + public function getMimeType($fileName) + { + $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($filePath); + + $result = $this->mime->getMimeType($absoluteFilePath); + return $result; + } + /** * Check if the file exists * diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index eaa2afc70cf60..bc275e28a99ee 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -37,6 +37,11 @@ class FileProcessorTest extends \PHPUnit_Framework_TestCase */ private $mediaDirectory; + /** + * @var \Magento\Framework\File\Mime|\PHPUnit_Framework_MockObject_MockObject + */ + private $mime; + protected function setUp() { $this->mediaDirectory = $this->getMockBuilder('Magento\Framework\Filesystem\Directory\WriteInterface') @@ -60,6 +65,10 @@ protected function setUp() $this->urlEncoder = $this->getMockBuilder('Magento\Framework\Url\EncoderInterface') ->getMockForAbstractClass(); + + $this->mime = $this->getMockBuilder(\Magento\Framework\File\Mime::class) + ->disableOriginalConstructor() + ->getMock(); } /** @@ -75,6 +84,7 @@ private function getModel($entityTypeCode, array $allowedExtensions = []) $this->urlBuilder, $this->urlEncoder, $entityTypeCode, + $this->mime, $allowedExtensions ); return $model; @@ -381,4 +391,26 @@ public function testMoveTemporaryFileWithException() $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $model->moveTemporaryFile($filePath); } + + public function testGetMimeType() + { + $fileName = '/filename.ext1'; + $absoluteFilePath = '/absolute_path/customer/filename.ext1'; + + $expected = 'ext1'; + + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . ltrim($fileName, '/')) + ->willReturn($absoluteFilePath); + + $this->mime->expects($this->once()) + ->method('getMimeType') + ->with($absoluteFilePath) + ->willReturn($expected); + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + + $this->assertEquals($expected, $model->getMimeType($fileName)); + } } From 6b804ddf032dc3badd998fdb41b7f566d82ba41a Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Thu, 27 Oct 2016 15:03:56 +0300 Subject: [PATCH 46/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 Remove BiC --- .../Model/Import/Product.php | 19 ---------------- .../Import/Product/Type/AbstractType.php | 22 ++++++++++++++++++- .../Model/Import/Product/Validator.php | 22 ++++++++++++++++++- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index d6164da5d9811..a95cda247ad53 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -2417,25 +2417,6 @@ private function parseAttributesWithWrappedValues($attributesData) return $attributes; } - /** - * Parse values of multiselect attributes depends on "Fields Enclosure" parameter - * - * @param string $values - * @return array - */ - public function parseMultiselectValues($values) - { - if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) { - return explode(self::PSEUDO_MULTI_LINE_SEPARATOR, $values); - } - if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { - return $values = array_map(function ($value) { - return str_replace('""', '"', $value); - }, $matches[1]); - } - return [$values]; - } - /** * Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 7a74e79d77b87..66f5cb7fcddb5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -483,7 +483,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; } elseif ('multiselect' == $attrParams['type']) { $resultAttrs[$attrCode] = []; - foreach ($this->_entityModel->parseMultiselectValues($rowData[$attrCode]) as $value) { + foreach ($this->parseMultiselectValues($this->_entityModel, $rowData[$attrCode]) as $value) { $resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)]; } $resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]); @@ -525,4 +525,24 @@ public function saveData() { return $this; } + + /** + * Parse values of multiselect attributes depends on "Fields Enclosure" parameter + * + * @param string $values + * @return array + */ + private function parseMultiselectValues(\Magento\CatalogImportExport\Model\Import\Product $context, $values) + { + $parameters = $context->getParameters(); + if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { + return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values); + } + if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { + return $values = array_map(function ($value) { + return str_replace('""', '"', $value); + }, $matches[1]); + } + return [$values]; + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 55b0781951b8d..1eb4c3f93999d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -190,7 +190,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) $valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]); break; case 'multiselect': - $values = $this->context->parseMultiselectValues($rowData[$attrCode]); + $values = $this->parseMultiselectValues($this->context, $rowData[$attrCode]); foreach ($values as $value) { $valid = $this->validateOption($attrCode, $attrParams['options'], $value); if (!$valid) { @@ -289,4 +289,24 @@ public function init($context) $validator->init($context); } } + + /** + * Parse values of multiselect attributes depends on "Fields Enclosure" parameter + * + * @param string $values + * @return array + */ + private function parseMultiselectValues(\Magento\CatalogImportExport\Model\Import\Product $context, $values) + { + $parameters = $context->getParameters(); + if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { + return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values); + } + if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { + return $values = array_map(function ($value) { + return str_replace('""', '"', $value); + }, $matches[1]); + } + return [$values]; + } } From dc9fdf4437f4682998d6f44520bce721dc613958 Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Thu, 27 Oct 2016 15:10:21 +0300 Subject: [PATCH 47/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 Remove BiC --- .../Model/Import/Product/Type/AbstractType.php | 9 ++++++--- .../Model/Import/Product/Validator.php | 5 ++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 66f5cb7fcddb5..c50d626bb08cc 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -483,7 +483,11 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; } elseif ('multiselect' == $attrParams['type']) { $resultAttrs[$attrCode] = []; - foreach ($this->parseMultiselectValues($this->_entityModel, $rowData[$attrCode]) as $value) { + $multiSelectValues = $this->parseMultiselectValues( + $this->_entityModel->getParameters(), + $rowData[$attrCode] + ); + foreach ($multiSelectValues as $value) { $resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)]; } $resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]); @@ -532,9 +536,8 @@ public function saveData() * @param string $values * @return array */ - private function parseMultiselectValues(\Magento\CatalogImportExport\Model\Import\Product $context, $values) + private function parseMultiselectValues(array $parameters, $values) { - $parameters = $context->getParameters(); if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values); } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 1eb4c3f93999d..0792ec487cb7d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -190,7 +190,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) $valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]); break; case 'multiselect': - $values = $this->parseMultiselectValues($this->context, $rowData[$attrCode]); + $values = $this->parseMultiselectValues($this->context->getParameters(), $rowData[$attrCode]); foreach ($values as $value) { $valid = $this->validateOption($attrCode, $attrParams['options'], $value); if (!$valid) { @@ -296,9 +296,8 @@ public function init($context) * @param string $values * @return array */ - private function parseMultiselectValues(\Magento\CatalogImportExport\Model\Import\Product $context, $values) + private function parseMultiselectValues(array $parameters, $values) { - $parameters = $context->getParameters(); if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values); } From 4f82185021c7932ce7485013ed784b2463850b6e Mon Sep 17 00:00:00 2001 From: Vladyslav Shcherbyna Date: Thu, 27 Oct 2016 15:11:45 +0300 Subject: [PATCH 48/48] MAGETWO-59413: [Backport] - [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |) - for 2.0 Remove BiC --- .../Model/Import/Product/Type/AbstractType.php | 5 +++-- .../CatalogImportExport/Model/Import/Product/Validator.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index c50d626bb08cc..45c659ca675c9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -483,7 +483,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; } elseif ('multiselect' == $attrParams['type']) { $resultAttrs[$attrCode] = []; - $multiSelectValues = $this->parseMultiselectValues( + $multiSelectValues = $this->parseMultiSelectValues( $this->_entityModel->getParameters(), $rowData[$attrCode] ); @@ -533,10 +533,11 @@ public function saveData() /** * Parse values of multiselect attributes depends on "Fields Enclosure" parameter * + * @param array $parameters * @param string $values * @return array */ - private function parseMultiselectValues(array $parameters, $values) + private function parseMultiSelectValues(array $parameters, $values) { if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 0792ec487cb7d..4c2b78c1cf45c 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -190,7 +190,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) $valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]); break; case 'multiselect': - $values = $this->parseMultiselectValues($this->context->getParameters(), $rowData[$attrCode]); + $values = $this->parseMultiSelectValues($this->context->getParameters(), $rowData[$attrCode]); foreach ($values as $value) { $valid = $this->validateOption($attrCode, $attrParams['options'], $value); if (!$valid) { @@ -293,10 +293,11 @@ public function init($context) /** * Parse values of multiselect attributes depends on "Fields Enclosure" parameter * + * @param array $parameters * @param string $values * @return array */ - private function parseMultiselectValues(array $parameters, $values) + private function parseMultiSelectValues(array $parameters, $values) { if (empty($parameters[\Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE])) { return explode(\Magento\CatalogImportExport\Model\Import\Product::PSEUDO_MULTI_LINE_SEPARATOR, $values);