diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3fcd973
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,61 @@
+language: php
+php:
+ - 7.3
+dist: xenial
+
+env:
+ matrix:
+ - TEST_GROUP=magento_latest
+ - TEST_GROUP=magento_23
+matrix:
+ exclude:
+ - php: 7.4
+ env: TEST_GROUP=magento_23
+ - php: 7.3
+ env: TEST_GROUP=magento_latest
+
+before_install:
+ - phpenv config-rm xdebug.ini || true
+ - composer self-update 1.10.16
+
+install:
+ - composer config repositories.foomanmirror composer https://repo-magento-mirror.fooman.co.nz/ # TODO this allows us to composer install, but we only need the dev dependency. Maybe wget it instead?
+ - composer install --no-interaction
+ # Install magento
+ - if [[ $TEST_GROUP = magento_23 ]]; then NAME=snowepr FULL_INSTALL=0 VERSION=2.3.6 . ./vendor/bin/travis-install-magento.sh; fi
+ - if [[ $TEST_GROUP = magento_latest ]]; then NAME=snowepr FULL_INSTALL=0 . ./vendor/bin/travis-install-magento.sh; fi
+ # Install this module
+ - cd vendor/ampersand/travis-vanilla-magento/instances/snowepr
+ - export COMPOSER_MEMORY_LIMIT=-1
+ - composer config repo.snowepr git "../../../../../"
+ - composer require -vvv snowio/magento2-extended-sales-repositories:"dev-$TRAVIS_BRANCH" || composer require -vvv snowio/magento2-extended-sales-repository:"$TRAVIS_BRANCH"
+ # Configure for integration tests
+ - mysql -uroot -e 'SET @@global.sql_mode = NO_ENGINE_SUBSTITUTION; DROP DATABASE IF EXISTS magento_integration_tests; CREATE DATABASE magento_integration_tests;'
+ - cp dev/tests/integration/etc/install-config-mysql.travis-no-rabbitmq.php.dist dev/tests/integration/etc/install-config-mysql.php
+ - php $TRAVIS_BUILD_DIR/travis/prepare_phpunit_config.php
+
+script:
+ - vendor/bin/phpunit -c $(pwd)/dev/tests/integration/phpunit.xml.dist --testsuite Integration
+
+addons:
+ apt:
+ packages:
+ - postfix
+ - apache2
+ - libapache2-mod-fastcgi
+
+services:
+ - mysql
+
+cache:
+ apt: true
+ directories:
+ - $HOME/.composer/cache
+ - $HOME/bin
+
+after_failure:
+ - test -d ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/report/ && for r in ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/report/*; do cat $r; done
+ - test -f ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/system.log && grep -v "Broken reference" ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/system.log
+ - test -f ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/exception.log && cat ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/exception.log
+ - test -f ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/support_report.log && grep -v "Broken reference" ./vendor/ampersand/travis-vanilla-magento/instances/snowepr/var/log/support_report.log
+ - sleep 10;
\ No newline at end of file
diff --git a/Exception/SnowCreditMemoException.php b/Exception/SnowCreditMemoException.php
new file mode 100644
index 0000000..49e7e71
--- /dev/null
+++ b/Exception/SnowCreditMemoException.php
@@ -0,0 +1,44 @@
+
+ */
+ public function provide(OrderInterface $order) : array
+ {
+ if (array_key_exists($order->getEntityId(), $this->availableByOrder)) {
+ return $this->availableByOrder[$order->getEntityId()];
+ }
+
+ $quantityRefunded = $this->getQuantityRefunded($order);
+
+ return $this->availableByOrder[$order->getEntityId()] = array_reduce(
+ array_keys($quantityRefunded),
+ static function (array $quantityAvailable, int $orderItemId) use ($quantityRefunded) : array {
+ if (!array_key_exists($orderItemId, $quantityAvailable)) {
+ return $quantityAvailable;
+ }
+
+ // Sum all quantities for the order item provided by the collected credit memos
+ $quantityAvailable[$orderItemId] =
+ max($quantityAvailable[$orderItemId] - array_sum($quantityRefunded[$orderItemId]), 0);
+
+ return $quantityAvailable;
+ },
+ $this->getQuantityAvailable($order)
+ );
+ }
+
+
+ /**
+ * Get quantity of items refunded in all credit memos
+ *
+ * @param \Magento\Sales\Api\Data\OrderInterface $order
+ * @return array
+ * @author Daniel Doyle
+ */
+ private function getQuantityRefunded(OrderInterface $order) : array
+ {
+ $refundedItems = [];
+ foreach ($order->getCreditmemosCollection() as $creditmemo) {
+ foreach ($creditmemo->getItems() as $creditmemoItem) {
+ $quantityRefunded = (float) $creditmemoItem->getQty();
+ if ($quantityRefunded < 1) {
+ continue;
+ }
+
+ // Dynamically create array of quantities for aggregation later
+ $refundedItems[$creditmemoItem->getOrderItemId()][] = $quantityRefunded;
+ }
+ }
+
+ return $refundedItems;
+ }
+
+ /**
+ * Get quantity of available items from order. As we're querying all credit memos (including broken/invalid ones) we
+ * base the refunded quantity on them instead of relying on `getQtyRefunded` set against an order item
+ *
+ * @param \Magento\Sales\Api\Data\OrderInterface $order
+ * @return array
+ * @author Daniel Doyle
+ */
+ private function getQuantityAvailable(OrderInterface $order) : array
+ {
+ return array_reduce(
+ $order->getAllItems(),
+ function (array $refundedItems, OrderItemInterface $orderItem) : array {
+ $refundedItems[$orderItem->getItemId()] = (float) $orderItem->getQtyOrdered();
+
+ return $refundedItems;
+ },
+ []
+ );
+ }
+}
diff --git a/Model/CreditmemoByOrderIncrementId.php b/Model/CreditmemoByOrderIncrementId.php
index 1861459..746441e 100644
--- a/Model/CreditmemoByOrderIncrementId.php
+++ b/Model/CreditmemoByOrderIncrementId.php
@@ -4,48 +4,76 @@
use Magento\Framework\Api\Filter;
use Magento\Framework\Api\Search\FilterGroup;
use Magento\Framework\Api\SearchCriteria;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Sales\Api\Data\CreditmemoInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Api\CreditmemoManagementInterface;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
-use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Creditmemo;
-use Magento\Sales\Model\Order\CreditmemoFactory;
+use Magento\Sales\Model\Order\Creditmemo\Item;
+use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender;
+use Psr\Log\LoggerInterface;
use SnowIO\ExtendedSalesRepositories\Api\CreditmemoByOrderIncrementIdInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\CreditmemoItemInterface;
use Magento\Sales\Api\Data\OrderItemInterface;
+use SnowIO\ExtendedSalesRepositories\Exception\SnowCreditMemoException;
class CreditmemoByOrderIncrementId implements CreditmemoByOrderIncrementIdInterface
{
- /** @var CreditmemoManagementInterface */
+ /**
+ * @var ExtendedCreditMemoFactory
+ */
+ private $extendedCreditMemoFactory;
+
+ /**
+ * @var CreditmemoSender
+ */
+ private $creditmemoSender;
+
+ /**
+ * @var CreditmemoManagementInterface
+ */
private $creditmemoManagement;
- /** @var CreditmemoRepositoryInterface */
+ /**
+ * @var CreditmemoRepositoryInterface
+ */
private $creditmemoRepository;
- /** @var OrderRepositoryInterface */
+ /**
+ * @var OrderRepositoryInterface
+ */
private $orderRepository;
- /** @var CreditmemoFactory */
- private $creditmemoFactory;
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
/**
* CreditmemoByOrderIncrementId constructor.
+ * @param ExtendedCreditMemoFactory $extendedCreditMemoFactory
+ * @param CreditmemoSender $creditmemoSender
* @param CreditmemoRepositoryInterface $creditmemoRepository
* @param CreditmemoManagementInterface $creditmemoManagement
* @param OrderRepositoryInterface $orderRepository
+ * @param LoggerInterface $logger
*/
public function __construct(
+ ExtendedCreditMemoFactory $extendedCreditMemoFactory,
+ CreditmemoSender $creditmemoSender,
CreditmemoRepositoryInterface $creditmemoRepository,
CreditmemoManagementInterface $creditmemoManagement,
OrderRepositoryInterface $orderRepository,
- CreditmemoFactory $creditmemoFactory
+ LoggerInterface $logger
) {
+ $this->extendedCreditMemoFactory = $extendedCreditMemoFactory;
+ $this->creditmemoSender = $creditmemoSender;
$this->creditmemoRepository = $creditmemoRepository;
$this->creditmemoManagement = $creditmemoManagement;
$this->orderRepository = $orderRepository;
- $this->creditmemoFactory = $creditmemoFactory;
+ $this->logger = $logger;
}
/**
@@ -54,55 +82,56 @@ public function __construct(
* @param string $orderIncrementId
* @param CreditmemoInterface $creditmemo
* @return CreditmemoInterface
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
public function createAndRefund(
$orderIncrementId,
CreditmemoInterface $creditmemo
) {
- /** @var Order $order */
$order = $this->loadOrderByIncrementId($orderIncrementId);
if(!$order->canCreditmemo()) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new SnowCreditMemoException(
__("This order does not allow creation of creditmemo")
);
}
- /** @var CreditmemoInterface $creditmemo */
- $newCreditmemo = $this->creditmemoFactory->createByOrder($order, [
- 'qtys' => $this->filterItemsToBeRefunded($order, $creditmemo)
- ]);
- $newCreditmemo->setState(Creditmemo::STATE_OPEN);
+ $newCreditmemo = $this->extendedCreditMemoFactory->create($order, $creditmemo);
$this->addBackToStockStatus($order, $creditmemo, $newCreditmemo);
+ $this->applyDefaults($newCreditmemo, $creditmemo);
+ $newCreditmemo->collectTotals();
+ $newCreditmemo->setState(Creditmemo::STATE_OPEN);
$this->creditmemoRepository->save($newCreditmemo);
- return $this->creditmemoManagement->refund($newCreditmemo);
+ $newCreditmemo = $this->creditmemoManagement->refund($newCreditmemo);
+
+ /**
+ * Called directly from with the controller
+ *
+ * @see \Magento\Sales\Controller\Adminhtml\Order\Creditmemo\Save
+ */
+ try {
+ /** @var Creditmemo $newCreditmemo */
+ $this->creditmemoSender->send($newCreditmemo);
+ } catch (\Exception $exception) {
+ $this->logger->error($exception);
+ }
+
+ return $newCreditmemo;
}
/**
- * Get the sku and qty from the input payload to decide which item and its quantity to be refunded.
- * This allow partial creditmemo/refund
- *
- * @param Order $order
+ * Applies defaults to the new credit memo
+ * @param CreditmemoInterface $newCreditmemo
* @param CreditmemoInterface $creditmemo
- * @return array
*/
- public function filterItemsToBeRefunded(Order $order, CreditmemoInterface $creditmemo)
+ public function applyDefaults(CreditmemoInterface $newCreditmemo, CreditmemoInterface $creditmemo)
{
- $selectedItemsToRefund = [];
- /** @var \Magento\Sales\Model\Order\Item $orderItem */
- foreach($order->getAllItems() as $orderItem){
- /** @var \Magento\Sales\Model\Order\Creditmemo\Item $inputItem */
- foreach($creditmemo->getItems() as $inputItem){
- if($orderItem->getSku() === $inputItem->getSku() && $inputItem->getQty() > 0){
- $selectedItemsToRefund[$orderItem->getId()] = $inputItem->getQty();
- }
- }
- }
- return $selectedItemsToRefund;
+ ExtendedCreditMemoManagement::applyAmounts($newCreditmemo, $creditmemo);
+ ExtendedCreditMemoManagement::applyTax($newCreditmemo, $creditmemo);
+ ExtendedCreditMemoManagement::applyExtensionAttributes($newCreditmemo, $creditmemo);
}
/**
@@ -110,12 +139,14 @@ public function filterItemsToBeRefunded(Order $order, CreditmemoInterface $credi
* @param CreditmemoInterface $creditmemo
* @param CreditmemoInterface $newCreditmemo
*/
- protected function addBackToStockStatus(OrderInterface $order, CreditmemoInterface $creditmemo, CreditmemoInterface $newCreditmemo)
- {
+ protected function addBackToStockStatus(
+ OrderInterface $order,
+ CreditmemoInterface $creditmemo,
+ CreditmemoInterface $newCreditmemo
+ ) {
$itemsToBackToStock = [];
- /** @var \Magento\Sales\Model\Order\Item $orderItem */
foreach ($order->getAllItems() as $orderItem){
- /** @var \Magento\Sales\Model\Order\Creditmemo\Item $creditmemoItem */
+ /** @var Item $creditmemoItem */
foreach ($creditmemo->getItems() as $creditmemoItem){
if (!$this->shouldGoBackToStock($creditmemoItem, $orderItem)) {
continue;
@@ -127,7 +158,7 @@ protected function addBackToStockStatus(OrderInterface $order, CreditmemoInterfa
$itemsToBackToStock = array_unique($itemsToBackToStock);
foreach($newCreditmemo->getItems() as $memoItem) {
- if (in_array($memoItem->getSku(), $itemsToBackToStock)) {
+ if (in_array($memoItem->getSku(), $itemsToBackToStock, true)) {
$memoItem->setBackToStock(true);
}
}
@@ -138,14 +169,14 @@ protected function addBackToStockStatus(OrderInterface $order, CreditmemoInterfa
* @param OrderItemInterface $orderItem
* @return bool
*/
- protected function shouldGoBackToStock(CreditmemoItemInterface $creditmemoItem, OrderItemInterface $orderItem): bool
+ protected function shouldGoBackToStock(CreditmemoItemInterface $creditmemoItem, OrderItemInterface $orderItem)
{
- $backToStockStatus = $creditmemoItem->getExtensionAttributes() ? $creditmemoItem->getExtensionAttributes()->getBackToStock() : 0;
+ $backToStockStatus = $creditmemoItem->getExtensionAttributes() ?
+ $creditmemoItem->getExtensionAttributes()->getBackToStock() : 0;
return $orderItem->getSku() === $creditmemoItem->getSku()
&& $backToStockStatus;
}
-
protected function loadOrderByIncrementId(string $incrementId)
{
$searchCriteria = (new SearchCriteria())
diff --git a/Model/ExtendedCreditMemoFactory.php b/Model/ExtendedCreditMemoFactory.php
new file mode 100644
index 0000000..5af1bcd
--- /dev/null
+++ b/Model/ExtendedCreditMemoFactory.php
@@ -0,0 +1,101 @@
+refundableItemsFilter = $refundableItemsFilter;
+ $this->creditmemoFactory = $creditmemoFactory;
+ }
+
+ /**
+ * Create a credit memo based on the order
+ * @param OrderInterface $order
+ * @param CreditmemoInterface $creditmemo
+ * @return Creditmemo
+ * @throws SnowCreditMemoException
+ * @author Alexander Wanyoike
+ */
+ public function create(OrderInterface $order, CreditmemoInterface $creditmemo)
+ {
+ $refundableItems = $this->refundableItemsFilter->filter($order, $creditmemo, $this->hasAdjustments($creditmemo));
+ if (empty($refundableItems) && !$this->hasAdjustments($creditmemo)) {
+ throw new SnowCreditMemoException(__('No items available to refund'));
+ }
+
+ if ($orderInvoice = $this->getLatestPaidInvoiceForOrder($order)) {
+ return $this->creditmemoFactory->createByInvoice($orderInvoice, [
+ 'qtys' => $refundableItems,
+ 'shipping_amount' => $creditmemo->getShippingAmount(),
+ 'adjustment_positive' => $creditmemo->getBaseAdjustmentPositive(),
+ 'adjustment_negative' => $creditmemo->getBaseAdjustmentNegative()
+ ]);
+ }
+
+ return $this->creditmemoFactory->createByOrder($order, [
+ 'qtys' => $refundableItems,
+ 'shipping_amount' => $creditmemo->getShippingAmount(),
+ 'adjustment_positive' => $creditmemo->getBaseAdjustmentPositive(),
+ 'adjustment_negative' => $creditmemo->getBaseAdjustmentNegative()
+ ]);
+ }
+
+ /**
+ * @param CreditmemoInterface $creditmemo
+ * @return bool
+ * @author Alexander Wanyoike
+ */
+ private function hasAdjustments(CreditmemoInterface $creditmemo)
+ {
+ return $creditmemo->getBaseAdjustmentNegative() !== null ||
+ $creditmemo->getBaseAdjustmentPositive() !== null ||
+ $creditmemo->getBaseAdjustment() !== null;
+ }
+
+ /**
+ * Get latest invoice for order
+ *
+ * @param OrderInterface $order
+ * @return Invoice|null
+ */
+ private function getLatestPaidInvoiceForOrder(OrderInterface $order)
+ {
+ /** @var Invoice $latestInvoice */
+ $latestInvoice = $order->getInvoiceCollection()
+ ->addAttributeToFilter('state', ['eq' => Invoice::STATE_PAID])
+ ->setPageSize(1)
+ ->setCurPage(1)
+ ->getLastItem();
+
+ return $latestInvoice->getId() !== null ? $latestInvoice : null;
+ }
+}
diff --git a/Model/ExtendedCreditMemoManagement.php b/Model/ExtendedCreditMemoManagement.php
new file mode 100644
index 0000000..44083d2
--- /dev/null
+++ b/Model/ExtendedCreditMemoManagement.php
@@ -0,0 +1,54 @@
+
+ */
+ public static function applyAmounts(CreditmemoInterface $creditmemo, CreditmemoInterface $inputCreditmemo)
+ {
+ $creditmemo
+ ->setShippingAmount($inputCreditmemo->getShippingAmount())
+ ->setBaseSubtotal($inputCreditmemo->getBaseSubtotal())
+ ->setBaseGrandTotal($inputCreditmemo->getBaseGrandTotal())
+ ->setGrandTotal($inputCreditmemo->getGrandTotal());
+ }
+
+ /**
+ * Apply tax calculations from the input
+ * @param CreditmemoInterface $creditmemo
+ * @param CreditmemoInterface $inputCreditmemo
+ * @author Alexander Wanyoike
+ */
+ public static function applyTax(CreditmemoInterface $creditmemo, CreditmemoInterface $inputCreditmemo)
+ {
+ $creditmemo->setBaseTaxAmount($inputCreditmemo->getBaseTaxAmount())
+ ->setTaxAmount($inputCreditmemo->getTaxAmount())
+ ->setBaseShippingTaxAmount($inputCreditmemo->getBaseShippingTaxAmount())
+ ->setShippingTaxAmount($inputCreditmemo->getShippingTaxAmount())
+ ->setShippingInclTax($inputCreditmemo->getShippingInclTax())
+ ->setBaseSubtotalInclTax($inputCreditmemo->getBaseSubtotalInclTax());
+ }
+
+ public static function applyExtensionAttributes(
+ CreditmemoInterface $creditmemo,
+ CreditmemoInterface $inputCreditMemo
+ ) {
+ $extensionAttributes = $inputCreditMemo->getExtensionAttributes();
+ if (!$extensionAttributes) {
+ return;
+ }
+ $creditmemo->setExtensionAttributes($extensionAttributes);
+ }
+}
diff --git a/Model/LoadOrderByIncrementIdTrait.php b/Model/LoadOrderByIncrementIdTrait.php
new file mode 100644
index 0000000..8fdaeb5
--- /dev/null
+++ b/Model/LoadOrderByIncrementIdTrait.php
@@ -0,0 +1,30 @@
+setFilterGroups([
+ (new FilterGroup)->setFilters([
+ (new Filter)
+ ->setField('increment_id')
+ ->setConditionType('eq')
+ ->setValue($incrementId)
+ ])
+ ]);
+
+ $order = $this->orderRepository->getList($searchCriteria)->getItems();
+
+ if (empty($order)) {
+ throw new \LogicException("No order exists with increment ID '$incrementId'.");
+ }
+
+ return reset($order);
+ }
+}
diff --git a/Model/RefundableItemsFilter.php b/Model/RefundableItemsFilter.php
new file mode 100644
index 0000000..62f8651
--- /dev/null
+++ b/Model/RefundableItemsFilter.php
@@ -0,0 +1,115 @@
+availableQuantityProvider = $availableQuantityProvider;
+ }
+
+ /**
+ * Filter valid items to refund
+ *
+ * @param OrderInterface $order
+ * @param CreditmemoInterface $creditmemo
+ * @param bool $hasAdjustment
+ * @return array
+ * @author Daniel Doyle
+ */
+ public function filter(OrderInterface $order, CreditmemoInterface $creditmemo, $hasAdjustment)
+ {
+ $availableQuantity = $this->availableQuantityProvider->provide($order);
+
+ if ($hasAdjustment) {
+ return !empty($creditmemo->getItems()) ?
+ $this->determineValidItemsToRefund($order, $creditmemo, $availableQuantity, $hasAdjustment) :
+ $this->getItemsWithoutQuantities($order);
+ }
+
+ return $this->determineValidItemsToRefund($order, $creditmemo, $availableQuantity);
+ }
+
+ /**
+ * @param OrderInterface $order
+ * @param CreditmemoInterface $creditmemo
+ * @param array $availableQuantity
+ * @param bool $isAdjustment
+ * @return array
+ * @author Alexander Wanyoike
+ */
+ private function determineValidItemsToRefund(
+ OrderInterface $order,
+ CreditmemoInterface $creditmemo,
+ array $availableQuantity,
+ $isAdjustment = false
+ ) {
+ $validItemsToRefund = [];
+ foreach ($order->getAllItems() as $orderItem) {
+ foreach ($creditmemo->getItems() as $creditmemoItem) {
+ if ($this->cantBeRefunded($orderItem, $creditmemoItem, $availableQuantity)) {
+ continue;
+ }
+
+ $validItemsToRefund[$orderItem->getId()] = $isAdjustment ? 0 : $creditmemoItem->getQty();
+ }
+ }
+ return $validItemsToRefund;
+ }
+
+ /**
+ * @param OrderItemInterface $orderItem
+ * @param CreditmemoItemInterface $creditmemoItem
+ * @param array $availableQuantity
+ * @return bool
+ * @author Alexander Wanyoike
+ */
+ private function cantBeRefunded(
+ OrderItemInterface $orderItem,
+ CreditmemoItemInterface $creditmemoItem,
+ array $availableQuantity
+ ) {
+ return $orderItem->getSku() !== $creditmemoItem->getSku()
+ || $creditmemoItem->getQty() <= 0
+ || $creditmemoItem->getQty() > $availableQuantity[$orderItem->getId()];
+ }
+
+ /**
+ * If the credit memo is an adjustment
+ * We will not do any returns thus all specified skus
+ * in the order will have a zero quantity.
+ * @param OrderInterface $order
+ * @return array
+ * @author Alexander Wanyoike
+ */
+ private function getItemsWithoutQuantities(OrderInterface $order)
+ {
+ $validItemsToRefund = [];
+ foreach ($order->getAllItems() as $orderItem) {
+ $validItemsToRefund[$orderItem->getId()] = 0;
+ }
+
+ return $validItemsToRefund;
+ }
+}
diff --git a/Model/ShipOrderByIncrementId.php b/Model/ShipOrderByIncrementId.php
index a2edc1e..a233eee 100644
--- a/Model/ShipOrderByIncrementId.php
+++ b/Model/ShipOrderByIncrementId.php
@@ -1,15 +1,13 @@
setFilterGroups([
- (new FilterGroup)->setFilters([
- (new Filter)
- ->setField('increment_id')
- ->setConditionType('eq')
- ->setValue($incrementId)
- ])
- ]);
-
- $order = $this->orderRepository->getList($searchCriteria)->getItems();
-
- if (empty($order)) {
- throw new \LogicException("No order exists with increment ID '$incrementId'.");
- }
-
- return reset($order);
- }
}
diff --git a/Test/Integration/Model/CreditmemoByOrderIncrementIdTest.php b/Test/Integration/Model/CreditmemoByOrderIncrementIdTest.php
new file mode 100644
index 0000000..e6d1620
--- /dev/null
+++ b/Test/Integration/Model/CreditmemoByOrderIncrementIdTest.php
@@ -0,0 +1,128 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->creditmemoRepository = $this->objectManager->get(CreditmemoRepositoryInterface::class);
+ $this->creditmemoByOrderIncrementId = $this->objectManager->get(CreditmemoByOrderIncrementIdInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture SnowIO_ExtendedSalesRepositories::Test/Integration/_files/order.php
+ * @dataProvider getStandardCaseTestData
+ * @param string $orderIncrementId
+ * @param CreditmemoInterface $creditmemo
+ * @param callable[] $assertions
+ */
+ public function testStandardCase(CreditmemoInterface $creditmemo, $assertions)
+ {
+ /** @var CreditmemoInterface $creditmemo */
+ $creditmemo = $this->creditmemoByOrderIncrementId->createAndRefund("100000001", $creditmemo);
+ foreach ($assertions as $assertion) {
+ $assertion($creditmemo);
+ }
+ }
+
+
+ /**
+ * @magentoDataFixture SnowIO_ExtendedSalesRepositories::Test/Integration/_files/order.php
+ * @dataProvider getErroneousCaseTestData
+ */
+ public function testErroneousCase(CreditmemoInterface $creditmemo, string $errorClass)
+ {
+ self::expectException($errorClass);
+ $this->creditmemoByOrderIncrementId->createAndRefund("100000001", $creditmemo);
+ }
+
+ public function getStandardCaseTestData()
+ {
+ return [
+ "should create and refund the credit memo using the order increment id" => [
+ $this->objectManager->create(CreditmemoInterface::class)
+ ->setItems([
+ $this->objectManager->create(CreditmemoItemInterface::class)
+ ->setSku('simple')
+ ->setQty(1)
+ ]),
+ [
+ $this->assertExistsInRepository(),
+ $this->assertItemsRefunded()
+ ]
+ ],
+ "should create and refund an adhoc adjustment credit memo using the order increment id" => [
+ $this->objectManager->create(CreditmemoInterface::class)
+ ->setAdjustmentPositive(50),
+ [
+ $this->assertExistsInRepository(),
+ $this->assertAdjustmentAmount(50.0),
+ ]
+ ]
+ ];
+ }
+
+ public function getErroneousCaseTestData()
+ {
+ return [
+ "should throw if no credit memo has no items and is not an adjustment" => [
+ $this->objectManager->create(CreditmemoInterface::class),
+ SnowCreditMemoException::class
+ ]
+ ];
+ }
+
+ private function assertExistsInRepository()
+ {
+ return function (CreditmemoInterface $creditmemo) {
+ $retrievedCreditmemo = $this->creditmemoRepository->get($creditmemo->getEntityId());
+ self::assertNotEmpty($retrievedCreditmemo);
+ };
+ }
+
+ private function assertItemsRefunded()
+ {
+ return function (CreditmemoInterface $creditmemo) {
+ $retrievedCreditmemo = $this->creditmemoRepository->get($creditmemo->getEntityId());
+ $itemsBySku = [];
+ foreach ($retrievedCreditmemo->getItems() as $retrievedCreditMemoItem) {
+ $itemsBySku[$retrievedCreditMemoItem->getSku()] = [
+ 'qty' => $retrievedCreditMemoItem->getQty()
+ ];
+ }
+
+ foreach ($creditmemo->getItems() as $creditmemoItem) {
+ self::assertNotEmpty($itemsBySku[$creditmemoItem->getSku()]);
+ self::assertEquals($creditmemoItem->getQty(), $itemsBySku[$creditmemoItem->getSku()]['qty']);
+ }
+ };
+ }
+
+ private function assertAdjustmentAmount(float $amount)
+ {
+ return function (CreditmemoInterface $creditmemo) use ($amount) {
+ $retrievedCreditmemo = $this->creditmemoRepository->get($creditmemo->getEntityId());
+ self::assertEquals($amount, $retrievedCreditmemo->getBaseAdjustmentPositive());
+ };
+ }
+}
diff --git a/Test/Integration/Model/ShipOrderByIncrementIdTest.php b/Test/Integration/Model/ShipOrderByIncrementIdTest.php
new file mode 100644
index 0000000..0740012
--- /dev/null
+++ b/Test/Integration/Model/ShipOrderByIncrementIdTest.php
@@ -0,0 +1,103 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->shipOrderByIncrementId = $this->objectManager->get(ShipOrderByIncrementIdInterface::class);
+ $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $this->shipmentRepository = $this->objectManager->get(ShipmentRepositoryInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture SnowIO_ExtendedSalesRepositories::Test/Integration/_files/order.php
+ */
+ public function testStandardCase()
+ {
+ $orderIncrementId = "100000001";
+ $order = $this->loadOrderByIncrementId($orderIncrementId);
+ $items = array_map(function (Item $orderItem) {
+ return $this->objectManager
+ ->create(ShipmentItemCreationInterface::class)
+ ->setQty(1)
+ ->setOrderItemId($orderItem->getId());
+ }, $order->getItems());
+ $shipmentId = $this->shipOrderByIncrementId->execute($orderIncrementId, $items);
+ $this->assertShipmentContainsCorrectItems($items, $shipmentId);
+
+ }
+
+ /**
+ * @magentoDataFixture SnowIO_ExtendedSalesRepositories::Test/Integration/_files/order.php
+ * @dataProvider getErroneousCaseTestData
+ * @param string $orderIncrementId
+ * @param array $items
+ * @param string $errorClass
+ */
+ public function testErroneousCase(string $orderIncrementId, array $items, string $errorClass)
+ {
+ $this->expectException($errorClass);
+ $this->shipOrderByIncrementId->execute($orderIncrementId, $items);
+ }
+
+ public function getErroneousCaseTestData()
+ {
+ return [
+ "should fail if no order with the increment id was found" => [
+ "123131415",
+ [
+ $this->objectManager->create(ShipmentItemCreationInterface::class)
+ ->setOrderItemId(1)
+ ->setQty(1)
+ ],
+ \LogicException::class,
+ ]
+ ];
+ }
+
+ public function assertShipmentContainsCorrectItems(array $items, string $shipmentId)
+ {
+ /** @var ShipmentInterface $shipment */
+ $shipment = $this->shipmentRepository->get($shipmentId);
+
+ $shipmentItemByOrderItemId = [];
+ foreach ($shipment->getItems() as $shipmentItem) {
+ $shipmentItemByOrderItemId[$shipmentItem->getOrderItemId()] = [
+ 'qty' => $shipmentItem->getQty(),
+ ];
+ }
+
+ $inputItemsBySku = [];
+ foreach ($items as $item) {
+ $inputItemsBySku[$item->getOrderItemId()] = [
+ 'qty' => $item->getQty()
+ ];
+ }
+
+ self::assertEquals($inputItemsBySku, $shipmentItemByOrderItemId);
+ }
+}
diff --git a/Test/Integration/Model/ShipmentRepositoryTest.php b/Test/Integration/Model/ShipmentRepositoryTest.php
deleted file mode 100644
index a52fbe8..0000000
--- a/Test/Integration/Model/ShipmentRepositoryTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-objectManager = Bootstrap::getObjectManager();
- $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
- }
-
- /**
- * @magentoDataFixture SnowIO/ExtendedSalesRepository/Test/Integration/_files/order.php
- */
- public function testStandardCase()
- {
- $order = $this->orderRepository->get(1);
-
- $payment = $order->getPayment();
- $paymentInfoBlock = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
- ->get('Magento\Payment\Helper\Data')
- ->getInfoBlock($payment);
- $payment->setBlockMock($paymentInfoBlock);
-
- /** @var ShipmentInterface $shipment */
- $shipment = $this->objectManager->create(ShipmentInterface::class);
- $shipment->setOrderId($order->getEntityId());
- $shipmentItem = $this->objectManager->create(ShipmentItemInterface::class);
- $shipmentItem->setOrderItem($order->getItems()[0]);
- $shipment->addItem($shipmentItem);
- $shipment->setPackages([['1'], ['2']]);
- $shipment->setShipmentStatus(\Magento\Sales\Model\Order\Shipment::STATUS_NEW);
- $shipment->save();
- }
-}
diff --git a/Test/Integration/_files/order.php b/Test/Integration/_files/order.php
index fc8ac4a..93747b3 100644
--- a/Test/Integration/_files/order.php
+++ b/Test/Integration/_files/order.php
@@ -1,37 +1,108 @@
get('Magento\Framework\Registry');
-$registry->unregister('isSecureArea');
-$registry->register('isSecureArea', true);
+/** @var \Magento\TestFramework\ObjectManager $objectManager */
+$objectManager = Bootstrap::getObjectManager();
-/** @var $order \Magento\Sales\Model\Order */
-$orderCollection = Bootstrap::getObjectManager()->create('Magento\Sales\Model\ResourceModel\Order\Collection');
-foreach ($orderCollection as $order) {
- $order->delete();
-}
+/** @var CategoryLinkManagementInterface $categoryLinkManagement */
+$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class);
-/** @var $product \Magento\Catalog\Model\Product */
-$productCollection = Bootstrap::getObjectManager()->create('Magento\Catalog\Model\ResourceModel\Product\Collection');
-foreach ($productCollection as $product) {
- $product->delete();
-}
+$tierPrices = [];
+/** @var ProductTierPriceInterfaceFactory $tierPriceFactory */
+$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class);
+/** @var $tpExtensionAttributes */
+$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class);
+/** @var $productExtensionAttributes */
+$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class);
-$registry->unregister('isSecureArea');
-$registry->register('isSecureArea', false);
+$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin');
+$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create()
+ ->setWebsiteId($adminWebsite->getId());
+$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create(
+ ['website_ids' => $adminWebsite->getId()]
+);
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => Group::CUST_GROUP_ALL,
+ 'qty' => 2,
+ 'value' => 8
+ ]
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
-/** @var \Magento\TestFramework\ObjectManager $objectManager */
-$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => Group::CUST_GROUP_ALL,
+ 'qty' => 5,
+ 'value' => 5
+ ]
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => Group::NOT_LOGGED_IN_ID,
+ 'qty' => 3,
+ 'value' => 5
+ ]
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => Group::NOT_LOGGED_IN_ID,
+ 'qty' => 3.2,
+ 'value' => 6,
+ ]
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
-/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */
-$categoryLinkManagement = $objectManager->create('Magento\Catalog\Api\CategoryLinkManagementInterface');
+$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create()
+ ->setWebsiteId($adminWebsite->getId())
+ ->setPercentageValue(50);
-/** @var $product \Magento\Catalog\Model\Product */
-$product = $objectManager->create('Magento\Catalog\Model\Product');
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => Group::NOT_LOGGED_IN_ID,
+ 'qty' => 10
+ ]
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes2);
+
+/** @var $product Product */
+$product = $objectManager->create(Product::class);
$product->isObjectNew(true);
-$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
+$product->setTypeId(Type::TYPE_SIMPLE)
->setId(1)
->setAttributeSetId(4)
->setWebsiteIds([1])
@@ -41,34 +112,14 @@
->setWeight(1)
->setShortDescription("Short description")
->setTaxClassId(0)
- ->setTierPrice(
- [
- [
- 'website_id' => 0,
- 'cust_group' => \Magento\Customer\Model\Group::CUST_GROUP_ALL,
- 'price_qty' => 2,
- 'price' => 8,
- ],
- [
- 'website_id' => 0,
- 'cust_group' => \Magento\Customer\Model\Group::CUST_GROUP_ALL,
- 'price_qty' => 5,
- 'price' => 5,
- ],
- [
- 'website_id' => 0,
- 'cust_group' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID,
- 'price_qty' => 3,
- 'price' => 5,
- ],
- ]
- )
+ ->setTierPrices($tierPrices)
->setDescription('Description with html tag')
+ ->setExtensionAttributes($productExtensionAttributesWebsiteIds)
->setMetaTitle('meta title')
->setMetaKeyword('meta keyword')
->setMetaDescription('meta description')
- ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
- ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
->setStockData(
[
'use_config_manage_stock' => 1,
@@ -109,14 +160,14 @@
'sort_order' => 0,
'values' => [
[
- 'option_type_id' => -1,
+ 'option_type_id' => null,
'title' => 'Option 1',
'price' => 3,
'price_type' => 'fixed',
'sku' => '3-1-select',
],
[
- 'option_type_id' => -1,
+ 'option_type_id' => null,
'title' => 'Option 2',
'price' => 3,
'price_type' => 'fixed',
@@ -132,14 +183,14 @@
'sort_order' => 0,
'values' => [
[
- 'option_type_id' => -1,
+ 'option_type_id' => null,
'title' => 'Option 1',
'price' => 3,
'price_type' => 'fixed',
'sku' => '4-1-radio',
],
[
- 'option_type_id' => -1,
+ 'option_type_id' => null,
'title' => 'Option 2',
'price' => 3,
'price_type' => 'fixed',
@@ -152,7 +203,7 @@
$options = [];
/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */
-$customOptionFactory = $objectManager->create('Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory');
+$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class);
foreach ($oldOptions as $option) {
/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */
@@ -164,85 +215,81 @@
$product->setOptions($options);
-/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */
-$productRepositoryFactory = $objectManager->create('Magento\Catalog\Api\ProductRepositoryInterface');
-$productRepositoryFactory->save($product);
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->create(ProductRepositoryInterface::class);
+$product = $productRepository->save($product);
+$indexerProcessor = $objectManager->get(Processor::class);
+$indexerProcessor->reindexRow($product->getId());
$categoryLinkManagement->assignProductToCategories(
$product->getSku(),
[2]
);
+
//Address
-$addressData = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Sales\Model\Order\Address');
-$addressData->setRegion(
- 'CA'
-)->setPostcode(
- '90210'
-)->setFirstname(
- 'a_unique_firstname'
-)->setLastname(
- 'lastname'
-)->setStreet(
- 'street'
-)->setCity(
- 'Beverly Hills'
-)->setEmail(
- 'admin@example.com'
-)->setTelephone(
- '1111111111'
-)->setCountryId(
- 'US'
-)->setAddressType(
- 'shipping'
-)->save();
-
-$billingAddress = $objectManager->create('Magento\Sales\Model\Order\Address', ['data' => $addressData]);
+$addressData = [
+ 'region' => 'CA',
+ 'region_id' => '12',
+ 'postcode' => '11111',
+ 'lastname' => 'lastname',
+ 'firstname' => 'firstname',
+ 'street' => 'street',
+ 'city' => 'Los Angeles',
+ 'email' => 'admin@example.com',
+ 'telephone' => '11111111',
+ 'country_id' => 'US'
+];
+
+$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]);
$billingAddress->setAddressType('billing');
$shippingAddress = clone $billingAddress;
$shippingAddress->setId(null)->setAddressType('shipping');
-$payment = $objectManager->create('Magento\Sales\Model\Order\Payment');
+/** @var Payment $payment */
+$payment = $objectManager->create(Payment::class);
$payment->setMethod('checkmo');
-/** @var \Magento\Sales\Model\Order\Item $orderItem */
-$orderItem = $objectManager->create('Magento\Sales\Model\Order\Item');
-$orderItem->setProductId($product->getId())->setQtyOrdered(2);
-$orderItem->setBasePrice($product->getPrice());
-$orderItem->setPrice($product->getPrice());
-$orderItem->setRowTotal($product->getPrice());
-$orderItem->setProductType('simple');
-
-/** @var \Magento\Sales\Model\Order $order */
-$order = $objectManager->create('Magento\Sales\Model\Order');
-$order->setIncrementId(
- '100000001'
-)->setState(
- \Magento\Sales\Model\Order::STATE_PROCESSING
-)->setStatus(
- $order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)
-)->setSubtotal(
- 100
-)->setGrandTotal(
- 100
-)->setBaseSubtotal(
- 100
-)->setBaseGrandTotal(
- 100
-)->setCustomerIsGuest(
- true
-)->setCustomerEmail(
- 'customer@null.com'
-)->setBillingAddress(
- $billingAddress
-)->setShippingAddress(
- $shippingAddress
-)->setStoreId(
- $objectManager->get('Magento\Store\Model\StoreManagerInterface')->getStore()->getId()
-)->addItem(
- $orderItem
-)->setPayment(
- $payment
-);
-$order->save();
+/** @var Item $orderItem */
+$orderItem = $objectManager->create(Item::class);
+$orderItem->setProductId($product->getId())
+ ->setQtyOrdered(2)
+ ->setBasePrice($product->getPrice())
+ ->setPrice($product->getPrice())
+ ->setRowTotal($product->getPrice())
+ ->setProductType('simple')
+ ->setName($product->getName())
+ ->setSku($product->getSku());
+
+/** @var Order $order */
+$order = $objectManager->create(Order::class);
+$order->setIncrementId('100000001')
+ ->setState(Order::STATE_PROCESSING)
+ ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING))
+ ->setSubtotal(100)
+ ->setGrandTotal(100)
+ ->setBaseSubtotal(100)
+ ->setBaseGrandTotal(100)
+ ->setCustomerIsGuest(true)
+ ->setCustomerEmail('customer@null.com')
+ ->setBillingAddress($billingAddress)
+ ->setShippingAddress($shippingAddress)
+ ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId())
+ ->addItem($orderItem)
+ ->setPayment($payment);
+
+
+
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->create(OrderRepositoryInterface::class);
+$orderRepository->save($order);
+
+/** @var InvoiceManagementInterface $orderService */
+$orderService = $objectManager->create(InvoiceManagementInterface::class);
+/** @var InvoiceInterface $invoice */
+$invoice = $orderService->prepareInvoice($order);
+$invoice->register();
+$order->setIsInProcess(true);
+$transactionSave = $objectManager->create(Transaction::class);
+$transactionSave->addObject($invoice)->addObject($order)->save();
\ No newline at end of file
diff --git a/Test/TestCase.php b/Test/TestCase.php
new file mode 100644
index 0000000..0acae41
--- /dev/null
+++ b/Test/TestCase.php
@@ -0,0 +1,14 @@
+=5.6",
"magento/module-sales": "^100.1.2|^101.0.2|^102.0.1",
"magento/framework": "^100.1.2|^101.0.2|^102.0.1"
},
+ "require-dev": {
+ "phpunit/phpunit": "^4.1|^6",
+ "ampersand/travis-vanilla-magento": "^1.0"
+ },
"autoload": {
"files": [ "registration.php" ],
"psr-4": {
diff --git a/travis/prepare_phpunit_config.php b/travis/prepare_phpunit_config.php
new file mode 100644
index 0000000..89cdc01
--- /dev/null
+++ b/travis/prepare_phpunit_config.php
@@ -0,0 +1,41 @@
+testsuites);
+$testsuiteNode = $config->addChild('testsuites')->addChild('testsuite');
+$testsuiteNode->addAttribute('name', 'Integration');
+$testsuiteNode->addChild('directory', "$travisBuildDir/Test/Integration")->addAttribute('suffix', 'Test.php');
+
+$codeCoverage = \getenv('CODE_COVERAGE');
+unset($config->logging);
+if ($codeCoverage) {
+ $logNode = $config->addChild('logging')->addChild('log');
+ $logNode->addAttribute('type', 'coverage-clover');
+ $logNode->addAttribute('target', "$travisBuildDir/coverage.xml");
+
+ unset($config->filter);
+ $whitelistNode = $config->addChild('filter')->addChild('whitelist');
+ $whitelistNode->addChild('directory', "../../../vendor/$packageName")->addAttribute('suffix', '.php');
+ $whitelistNode->addChild('exclude')->addChild('file', "../../../vendor/$packageName/registration.php");
+ $whitelistNode->addChild('exclude')->addChild('directory', "../../../vendor/$packageName/Setup");
+ $whitelistNode->addChild('exclude')->addChild('directory', "../../../vendor/$packageName/Test");
+ $whitelistNode->addChild('exclude')->addChild('directory', "../../../vendor/$packageName/travis");
+}
+
+$config->asXML($configPath);
\ No newline at end of file