diff --git a/.travis.yml b/.travis.yml index 30d7be0..855ea47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,8 @@ install: - composer create-project --repository=https://repo.magento.com magento/marketplace-eqp marketplace-eqp script: - - php marketplace-eqp/vendor/bin/phpcs Api/ Console/ Cron/ Helper/ Model/ Test/ --standard=MEQP2 --severity=10 - - php vendor/bin/phpmd Api/,Console/,Cron/,Helper/,Model/,Test/ text cleancode,codesize,controversial,design,naming,unusedcode --ignore-violations-on-exit + - php marketplace-eqp/vendor/bin/phpcs Api/ Block/ Console/ Cron/ Helper/ Model/ Test/ --standard=MEQP2 --severity=10 + - php vendor/bin/phpmd Api/,Block/,Console/,Cron/,Helper/,Model/,Test/ text cleancode,codesize,controversial,design,naming,unusedcode --ignore-violations-on-exit - php vendor/phpunit/phpunit/phpunit --coverage-clover Test/Unit/logs/clover.xml Test after_script: diff --git a/Api/Data/InvoiceProcessItemInterface.php b/Api/Data/InvoiceProcessItemInterface.php new file mode 100644 index 0000000..5ff021b --- /dev/null +++ b/Api/Data/InvoiceProcessItemInterface.php @@ -0,0 +1,37 @@ +paymentConfig = $paymentConfig; + + parent::__construct($context, $data); + } + + /** + * Render block HTML + * + * @return string + */ + protected function _toHtml() + { + if (!$this->getOptions()) { + $options = [ + ['value' => HelperData::RULE_PAYMENT_METHOD_ALL, 'label' => __('Any')] + ]; + + $paymentMethods = $this->paymentConfig->getActiveMethods(); + foreach ($paymentMethods as $code => $model) { + $options []= [ + 'value' => $code, + 'label' => $model->getTitle() ?: $code, + ]; + } + + $this->setOptions($options); + } + + return parent::_toHtml(); + } + + /** + * Sets name for input element + * + * @param string $value + * @return $this + */ + public function setInputName($value) + { + return $this->setName($value); + } +} diff --git a/Block/Adminhtml/Form/Field/ProcessingRule.php b/Block/Adminhtml/Form/Field/ProcessingRule.php new file mode 100644 index 0000000..c20cea8 --- /dev/null +++ b/Block/Adminhtml/Form/Field/ProcessingRule.php @@ -0,0 +1,130 @@ +srcStatusRenderer) { + $this->srcStatusRenderer = $this->getLayout()->createBlock( + Status::class, + '', + ['data' => ['is_render_to_js_template' => true]] + ); + } + + return $this->srcStatusRenderer; + } + + /** + * Returns renderer for destination status element + */ + protected function getDstStatusRenderer() + { + if (!$this->dstStatusRenderer) { + $this->dstStatusRenderer = $this->getLayout()->createBlock( + Status::class, + '', + ['data' => ['is_render_to_js_template' => true]] + ); + } + + return $this->dstStatusRenderer; + } + + /** + * Returns renderer for payment method + */ + protected function getPaymentMethodRenderer() + { + if (!$this->paymentMethodRenderer) { + $this->paymentMethodRenderer = $this->getLayout()->createBlock( + PaymentMethod::class, + '', + ['data' => ['is_render_to_js_template' => true]] + ); + } + return $this->paymentMethodRenderer; + } + + /** + * Prepare to render + * @return void + */ + protected function _prepareToRender() + { + $this->addColumn( + 'src_status', + [ + 'label' => __('Source Status'), + 'renderer' => $this->getSrcStatusRenderer(), + ] + ); + $this->addColumn( + 'payment_method', + [ + 'label' => __('Payment Method'), + 'renderer' => $this->getPaymentMethodRenderer(), + ] + ); + $this->addColumn( + 'dst_status', + [ + 'label' => __('Destination Status'), + 'renderer' => $this->getDstStatusRenderer(), + ] + ); + + $this->_addAfter = false; + $this->_addButtonLabel = __('Add Rule'); + } + + /** + * Prepare existing row data object + * + * @param DataObject $row + * @return void + */ + protected function _prepareArrayRow(DataObject $row) + { + $srcStatus = $row->getSrcStatus(); + $dstStatus = $row->getDstStatus(); + $paymentMethod = $row->getPaymentMethod(); + + $options = []; + if ($srcStatus) { + $options['option_' . $this->getSrcStatusRenderer()->calcOptionHash($srcStatus)] + = 'selected="selected"'; + + $options['option_' . $this->getDstStatusRenderer()->calcOptionHash($dstStatus)] + = 'selected="selected"'; + + $options['option_' . $this->getPaymentMethodRenderer()->calcOptionHash($paymentMethod)] + = 'selected="selected"'; + } + + $row->setData('option_extra_attrs', $options); + } +} diff --git a/Block/Adminhtml/Form/Field/Status.php b/Block/Adminhtml/Form/Field/Status.php new file mode 100644 index 0000000..3c9c888 --- /dev/null +++ b/Block/Adminhtml/Form/Field/Status.php @@ -0,0 +1,70 @@ +orderConfig = $orderConfig; + + parent::__construct($context, $data); + } + + /** + * Render block HTML + * + * @return string + */ + protected function _toHtml() + { + if (!$this->getOptions()) { + $statuses = $this->stateStatuses + ? $this->orderConfig->getStateStatuses($this->stateStatuses) + : $this->orderConfig->getStatuses(); + + $this->setOptions($statuses); + } + return parent::_toHtml(); + } + + /** + * Sets name for input element + * + * @param string $value + * @return $this + */ + public function setInputName($value) + { + return $this->setName($value); + } +} diff --git a/Console/ProcessCommand.php b/Console/ProcessCommand.php index a8d21ea..c3b3ec0 100644 --- a/Console/ProcessCommand.php +++ b/Console/ProcessCommand.php @@ -94,10 +94,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln('This is a dry run, no orders will actually be invoiced.'); } - $collection = $this->invoiceProcess->getOrdersToInvoice(); - foreach ($collection as $order) { + $items = $this->invoiceProcess->getItemsToProcess(); + foreach ($items as $item) { try { + $order = $item->getOrder(); $message = sprintf( 'Invoicing completed order #%s', $order->getIncrementId() @@ -109,7 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $this->logger->info($message); - $this->invoiceProcess->invoice($order); + $this->invoiceProcess->invoice($item); } catch (\Exception $ex) { $output->writeln(sprintf( diff --git a/Cron/InvoiceProcess.php b/Cron/InvoiceProcess.php index 1a132c8..0fa63db 100644 --- a/Cron/InvoiceProcess.php +++ b/Cron/InvoiceProcess.php @@ -48,16 +48,17 @@ public function execute() } $this->logger->info('Starting auto invoice procedure.'); - $collection = $this->invoiceProcess->getOrdersToInvoice(); + $items = $this->invoiceProcess->getItemsToProcess(); - foreach ($collection as $order) { + foreach ($items as $item) { try { + $order = $item->getOrder(); $this->logger->info(sprintf( 'Invoicing completed order #%s', $order->getIncrementId() )); - $this->invoiceProcess->invoice($order); + $this->invoiceProcess->invoice($item); } catch (\Exception $ex) { $this->logger->critical($ex->getMessage()); diff --git a/Helper/Data.php b/Helper/Data.php index 77d2a1e..127d684 100644 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -3,24 +3,40 @@ namespace Aune\AutoInvoice\Helper; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\Serializer\Json; class Data { const XML_PATH_CRON_ENABLED = 'sales/autoinvoice/cron_active'; - const XML_PATH_ORDER_STATUSES = 'sales/autoinvoice/statuses'; + const XML_PATH_PROCESSING_RULES = 'sales/autoinvoice/processing_rules'; + + const RULE_SOURCE_STATUS = 'src_status'; + const RULE_DESTINATION_STATUS = 'dst_status'; + const RULE_PAYMENT_METHOD = 'payment_method'; + const RULE_KEY_SEPARATOR = '|'; + const RULE_PAYMENT_METHOD_ALL = '*'; /** * @var ScopeConfigInterface */ private $scopeConfig; + /** + * @var Json + */ + private $serializer; + /** * @param ScopeConfigInterface $scopeConfig + * @param Json $serializer */ public function __construct( - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + Json $serializer ) { $this->scopeConfig = $scopeConfig; + $this->serializer = $serializer; } /** @@ -32,12 +48,23 @@ public function isCronEnabled() } /** - * Return statuses to process + * Return processing rules */ - public function getOrderStatuses() + public function getProcessingRules() { - $value = $this->scopeConfig->getValue(self::XML_PATH_ORDER_STATUSES); + $value = $this->scopeConfig->getValue(self::XML_PATH_PROCESSING_RULES); + $value = $value ? $this->serializer->unserialize($value) : []; + + $rules = []; + foreach ($value as $key => $dstStatus) { + $parts = explode(self::RULE_KEY_SEPARATOR, $key); + $rules []= [ + self::RULE_SOURCE_STATUS => $parts[0], + self::RULE_PAYMENT_METHOD => $parts[1], + self::RULE_DESTINATION_STATUS => $dstStatus, + ]; + } - return $value ? explode(',', $value) : []; + return $rules; } } diff --git a/Model/Adminhtml/System/Config/ProcessingRule.php b/Model/Adminhtml/System/Config/ProcessingRule.php new file mode 100644 index 0000000..473f6cc --- /dev/null +++ b/Model/Adminhtml/System/Config/ProcessingRule.php @@ -0,0 +1,123 @@ +mathRandom = $mathRandom; + $this->serializer = $serializer; + + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + } + + /** + * Prepare data before save + * + * @return $this + */ + public function beforeSave() + { + $value = $this->getValue(); + $result = []; + + foreach ($value as $data) { + if (empty($data[HelperData::RULE_SOURCE_STATUS]) + || empty($data[HelperData::RULE_PAYMENT_METHOD]) + || empty($data[HelperData::RULE_DESTINATION_STATUS])) { + + continue; + } + + $key = implode(HelperData::RULE_KEY_SEPARATOR, [ + $data[HelperData::RULE_SOURCE_STATUS], + $data[HelperData::RULE_PAYMENT_METHOD], + ]); + $result[$key] = $data[HelperData::RULE_DESTINATION_STATUS]; + } + + $this->setValue($this->serializer->serialize($result)); + + return $this; + } + + /** + * Process data after load + * + * @return $this + */ + public function afterLoad() + { + if ($this->getValue()) { + $value = $this->serializer->unserialize($this->getValue()); + if (is_array($value)) { + $this->setValue($this->encodeArrayFieldValue($value)); + } + } + return $this; + } + + /** + * Encode value to be used in \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray + * + * @param array $value + * @return array + */ + protected function encodeArrayFieldValue(array $value) + { + $result = []; + foreach ($value as $key => $dstStatus) { + $parts = explode(HelperData::RULE_KEY_SEPARATOR, $key); + $id = $this->mathRandom->getUniqueHash('_'); + + $result[$id] = [ + HelperData::RULE_SOURCE_STATUS => $parts[0], + HelperData::RULE_PAYMENT_METHOD => $parts[1], + HelperData::RULE_DESTINATION_STATUS => $dstStatus, + ]; + } + return $result; + } +} diff --git a/Model/Config/Source/Order/Status.php b/Model/Config/Source/Order/Status.php deleted file mode 100644 index afd495d..0000000 --- a/Model/Config/Source/Order/Status.php +++ /dev/null @@ -1,24 +0,0 @@ -_stateStatuses - ? $this->_orderConfig->getStateStatuses($this->_stateStatuses) - : $this->_orderConfig->getStatuses(); - - foreach ($statuses as $code => $label) { - $options[] = ['value' => $code, 'label' => $label]; - } - return $options; - } -} diff --git a/Model/InvoiceProcess.php b/Model/InvoiceProcess.php index e901671..6ca9720 100644 --- a/Model/InvoiceProcess.php +++ b/Model/InvoiceProcess.php @@ -6,9 +6,11 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Invoice as OrderInvoice; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; -use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Model\Service\InvoiceServiceFactory; use Aune\AutoInvoice\Api\InvoiceProcessInterface; +use Aune\AutoInvoice\Api\Data\InvoiceProcessItemInterface; +use Aune\AutoInvoice\Api\Data\InvoiceProcessItemInterfaceFactory; use Aune\AutoInvoice\Helper\Data as HelperData; /** @@ -26,52 +28,94 @@ class InvoiceProcess implements InvoiceProcessInterface */ private $orderCollectionFactory; + /** + * @var InvoiceProcessItemInterfaceFactory + */ + private $invoiceProcessItemFactory; + /** * @var Transaction */ private $transaction; /** - * @var InvoiceService + * @var InvoiceServiceFactory */ - private $invoiceService; + private $invoiceServiceFactory; /** * @param HelperData $helperData * @param OrderCollectionFactory $orderCollectionFactory + * @param InvoiceProcessItemInterfaceFactory $invoiceProcessItemFactory * @param Transaction $transaction - * @param InvoiceService $invoiceService + * @param InvoiceServiceFactory $invoiceServiceFactory */ public function __construct( HelperData $helperData, OrderCollectionFactory $orderCollectionFactory, + InvoiceProcessItemInterfaceFactory $invoiceProcessItemFactory, Transaction $transaction, - InvoiceService $invoiceService + InvoiceServiceFactory $invoiceServiceFactory ) { $this->helperData = $helperData; $this->orderCollectionFactory = $orderCollectionFactory; + $this->invoiceProcessItemFactory = $invoiceProcessItemFactory; $this->transaction = $transaction; - $this->invoiceService = $invoiceService; + $this->invoiceServiceFactory = $invoiceServiceFactory; } /** * @inheritdoc */ - public function getOrdersToInvoice() + public function getItemsToProcess() { - $statuses = $this->helperData->getOrderStatuses(); + $items = []; + $rules = $this->helperData->getProcessingRules(); - return $this->orderCollectionFactory->create() - ->addFieldToFilter('status', ['in' => $statuses]) - ->addFieldToFilter('total_invoiced', ['null' => true]); + foreach ($rules as $rule) { + $collection = $this->orderCollectionFactory->create() + ->addFieldToFilter('status', ['eq' => $rule[HelperData::RULE_SOURCE_STATUS]]) + ->addFieldToFilter('total_invoiced', ['null' => true]); + + foreach ($collection as $order) { + if ($rule[HelperData::RULE_PAYMENT_METHOD] != HelperData::RULE_PAYMENT_METHOD_ALL + && $rule[HelperData::RULE_PAYMENT_METHOD] != $this->getPaymentMethodCode($order)) { + + continue; + } + + $items[$order->getId()] = $this->invoiceProcessItemFactory->create() + ->setOrder($order) + ->setDestinationStatus($rule[HelperData::RULE_DESTINATION_STATUS]); + } + } + + return $items; + } + + /** + * Returns payment method code of the given order + */ + private function getPaymentMethodCode(Order $order) + { + try { + return $order->getPayment()->getMethodInstance()->getCode(); + } catch (\Exception $ex) { + return ''; + } } /** * @inheritdoc */ - public function invoice(Order $order) + public function invoice(InvoiceProcessItemInterface $item) { - $invoice = $this->invoiceService->prepareInvoice($order); + $order = $item->getOrder(); + + $order->setStatus($item->getDestinationStatus()); + + $invoice = $this->invoiceServiceFactory->create() + ->prepareInvoice($order); $invoice->setRequestedCaptureCase(OrderInvoice::CAPTURE_OFFLINE); $invoice->register(); diff --git a/Model/InvoiceProcessItem.php b/Model/InvoiceProcessItem.php new file mode 100644 index 0000000..98d68de --- /dev/null +++ b/Model/InvoiceProcessItem.php @@ -0,0 +1,41 @@ +getData(self::KEY_ORDER); + } + + /** + * @inheritdoc + */ + public function setOrder(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->setData(self::KEY_ORDER, $order); + } + + /** + * @inheritdoc + */ + public function getDestinationStatus() + { + return $this->getData(self::KEY_DESTINATION_STATUS); + } + + /** + * @inheritdoc + */ + public function setDestinationStatus(string $status) + { + return $this->setData(self::KEY_DESTINATION_STATUS, $status); + } +} diff --git a/Test/Unit/Console/ProcessCommandTest.php b/Test/Unit/Console/ProcessCommandTest.php index 2db04ec..cc569f7 100644 --- a/Test/Unit/Console/ProcessCommandTest.php +++ b/Test/Unit/Console/ProcessCommandTest.php @@ -7,6 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Magento\Framework\App\State; use Magento\Sales\Model\Order; +use Aune\AutoInvoice\Api\Data\InvoiceProcessItemInterface; use Aune\AutoInvoice\Api\InvoiceProcessInterface; use Aune\AutoInvoice\Console\ProcessCommand; @@ -41,11 +42,8 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->invoiceProcessMock = $this->createMock(InvoiceProcessInterface::class); + $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->invoiceProcessMock = $this->getMockForAbstractClass(InvoiceProcessInterface::class); $this->processCommand = new ProcessCommand( $this->stateMock, @@ -88,20 +86,23 @@ public function testExecuteDryRun() ->disableOriginalConstructor() ->getMock(); - $n = 10; - $orderMocks = []; - - for ($i=0; $i<$n; $i++) { + $itemMocks = []; + for ($i=0; $i<10; $i++) { $orderMock = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() ->getMock(); - $orderMocks []= $orderMock; + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + $itemMock->expects(self::any()) + ->method('getOrder') + ->willReturn($orderMock); + + $itemMocks []= $itemMock; } $this->invoiceProcessMock->expects(self::once()) - ->method('getOrdersToInvoice') - ->willReturn($orderMocks); + ->method('getItemsToProcess') + ->willReturn($itemMocks); $this->invoiceProcessMock->expects(self::exactly(0)) ->method('invoice'); @@ -127,22 +128,25 @@ public function testExecute() ->disableOriginalConstructor() ->getMock(); - $n = 10; - $orderMocks = []; - - for ($i=0; $i<$n; $i++) { + $itemMocks = []; + for ($i=0; $i<10; $i++) { $orderMock = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() ->getMock(); - $orderMocks []= $orderMock; + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + $itemMock->expects(self::any()) + ->method('getOrder') + ->willReturn($orderMock); + + $itemMocks []= $itemMock; } $this->invoiceProcessMock->expects(self::once()) - ->method('getOrdersToInvoice') - ->willReturn($orderMocks); + ->method('getItemsToProcess') + ->willReturn($itemMocks); - $this->invoiceProcessMock->expects(self::exactly(count($orderMocks))) + $this->invoiceProcessMock->expects(self::exactly(count($itemMocks))) ->method('invoice'); $this->processCommand->run($inputMock, $outputMock); diff --git a/Test/Unit/Cron/InvoiceProcessTest.php b/Test/Unit/Cron/InvoiceProcessTest.php index 5713367..a7f3150 100644 --- a/Test/Unit/Cron/InvoiceProcessTest.php +++ b/Test/Unit/Cron/InvoiceProcessTest.php @@ -1,9 +1,10 @@ disableOriginalConstructor() ->getMock(); - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->invoiceProcessMock = $this->createMock(InvoiceProcessInterface::class); + $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->invoiceProcessMock = $this->getMockForAbstractClass(InvoiceProcessInterface::class); $this->invoiceProcess = new InvoiceProcess( $this->helperDataMock, @@ -62,7 +60,7 @@ public function testExecuteDisabled() ->willReturn(false); $this->invoiceProcessMock->expects(self::exactly(0)) - ->method('getOrdersToInvoice'); + ->method('getItemsToProcess'); $this->invoiceProcessMock->expects(self::exactly(0)) ->method('invoice'); @@ -79,22 +77,25 @@ public function testExecute() ->method('isCronEnabled') ->willReturn(true); - $n = 10; - $orderMocks = []; - - for ($i=0; $i<$n; $i++) { + $itemMocks = []; + for ($i=0; $i<10; $i++) { $orderMock = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() ->getMock(); - $orderMocks []= $orderMock; + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + $itemMock->expects(self::any()) + ->method('getOrder') + ->willReturn($orderMock); + + $itemMocks []= $itemMock; } $this->invoiceProcessMock->expects(self::once()) - ->method('getOrdersToInvoice') - ->willReturn($orderMocks); + ->method('getItemsToProcess') + ->willReturn($itemMocks); - $this->invoiceProcessMock->expects(self::exactly(count($orderMocks))) + $this->invoiceProcessMock->expects(self::exactly(count($itemMocks))) ->method('invoice'); $this->invoiceProcess->execute(); diff --git a/Test/Unit/Helper/DataTest.php b/Test/Unit/Helper/DataTest.php index 19aa055..62116eb 100644 --- a/Test/Unit/Helper/DataTest.php +++ b/Test/Unit/Helper/DataTest.php @@ -3,6 +3,7 @@ namespace Aune\AutoInvoice\Test\Unit\Helper; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; use Aune\AutoInvoice\Helper\Data as HelperData; /** @@ -25,7 +26,8 @@ protected function setUp() $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->helperData = new HelperData( - $this->scopeConfigMock + $this->scopeConfigMock, + new Json() ); } @@ -53,8 +55,64 @@ public function getConfigDataProvider() return [ ['key' => HelperData::XML_PATH_CRON_ENABLED, 'isFlag' => true, 'method' => 'isCronEnabled', 'in' => '1', 'out' => true], ['key' => HelperData::XML_PATH_CRON_ENABLED, 'isFlag' => true, 'method' => 'isCronEnabled', 'in' => '0', 'out' => false], - ['key' => HelperData::XML_PATH_ORDER_STATUSES, 'isFlag' => false, 'method' => 'getOrderStatuses', 'in' => '', 'out' => []], - ['key' => HelperData::XML_PATH_ORDER_STATUSES, 'isFlag' => false, 'method' => 'getOrderStatuses', 'in' => 'a,b', 'out' => ['a', 'b']], + ['key' => HelperData::XML_PATH_PROCESSING_RULES, 'isFlag' => false, 'method' => 'getProcessingRules', 'in' => '', 'out' => []], ]; } + + /** + * @dataProvider getProcessingRulesDataProvider + */ + public function testGetProcessingRules($in, $out) + { + $this->scopeConfigMock->expects(self::once()) + ->method('getValue') + ->with(HelperData::XML_PATH_PROCESSING_RULES) + ->willReturn($in); + + self::assertEquals( + $out, + $this->helperData->getProcessingRules() + ); + } + + /** + * @return array + */ + public function getProcessingRulesDataProvider() + { + return [[ + 'in' => '{"pending|*":"complete"}', + 'out' => [[ + HelperData::RULE_SOURCE_STATUS => 'pending', + HelperData::RULE_PAYMENT_METHOD => HelperData::RULE_PAYMENT_METHOD_ALL, + HelperData::RULE_DESTINATION_STATUS => 'complete', + ]], + ], [ + 'in' => '{"pending|*":"complete","pending|free":"processing"}', + 'out' => [[ + HelperData::RULE_SOURCE_STATUS => 'pending', + HelperData::RULE_PAYMENT_METHOD => HelperData::RULE_PAYMENT_METHOD_ALL, + HelperData::RULE_DESTINATION_STATUS => 'complete', + ], [ + HelperData::RULE_SOURCE_STATUS => 'pending', + HelperData::RULE_PAYMENT_METHOD => 'free', + HelperData::RULE_DESTINATION_STATUS => 'processing', + ]], + ], [ + 'in' => '{"pending|*":"complete","processing|*":"complete","pending|free":"processing"}', + 'out' => [[ + HelperData::RULE_SOURCE_STATUS => 'pending', + HelperData::RULE_PAYMENT_METHOD => HelperData::RULE_PAYMENT_METHOD_ALL, + HelperData::RULE_DESTINATION_STATUS => 'complete', + ], [ + HelperData::RULE_SOURCE_STATUS => 'processing', + HelperData::RULE_PAYMENT_METHOD => HelperData::RULE_PAYMENT_METHOD_ALL, + HelperData::RULE_DESTINATION_STATUS => 'complete', + ], [ + HelperData::RULE_SOURCE_STATUS => 'pending', + HelperData::RULE_PAYMENT_METHOD => 'free', + HelperData::RULE_DESTINATION_STATUS => 'processing', + ]], + ]]; + } } diff --git a/Test/Unit/Model/InvoiceProcessItemTest.php b/Test/Unit/Model/InvoiceProcessItemTest.php new file mode 100644 index 0000000..6b019ee --- /dev/null +++ b/Test/Unit/Model/InvoiceProcessItemTest.php @@ -0,0 +1,67 @@ +invoiceProcessItem = new InvoiceProcessItem(); + } + + /** + * Test class service contract + */ + public function testServiceContract() + { + $this->assertInstanceOf( + InvoiceProcessItemInterface::class, + $this->invoiceProcessItem + ); + } + + /** + * @dataProvider getFieldsDataProvider + */ + public function testGettersAndSetters($field, $getter, $setter, $value) + { + $this->assertEquals( + $this->invoiceProcessItem->$setter($value), + $this->invoiceProcessItem + ); + + $this->assertEquals( + $this->invoiceProcessItem->getData($field), + $value + ); + + $this->assertEquals( + $this->invoiceProcessItem->$getter(), + $value + ); + } + + /** + * @return array + */ + public function getFieldsDataProvider() + { + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + return [ + ['field' => InvoiceProcessItemInterface::KEY_ORDER, 'getter' => 'getOrder', 'setter' => 'setOrder', 'value' => $orderMock], + ['field' => InvoiceProcessItemInterface::KEY_DESTINATION_STATUS, 'getter' => 'getDestinationStatus', 'setter' => 'setDestinationStatus', 'value' => 'complete'], + ]; + } +} diff --git a/Test/Unit/Model/InvoiceProcessTest.php b/Test/Unit/Model/InvoiceProcessTest.php index 90c673c..60e59cb 100644 --- a/Test/Unit/Model/InvoiceProcessTest.php +++ b/Test/Unit/Model/InvoiceProcessTest.php @@ -8,7 +8,10 @@ use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Model\Service\InvoiceServiceFactory; +use Aune\AutoInvoice\Api\Data\InvoiceProcessItemInterface; +use Aune\AutoInvoice\Api\Data\InvoiceProcessItemInterfaceFactory; use Aune\AutoInvoice\Api\InvoiceProcessInterface; use Aune\AutoInvoice\Helper\Data as HelperData; use Aune\AutoInvoice\Model\InvoiceProcess; @@ -25,15 +28,20 @@ class InvoiceProcessTest extends \PHPUnit\Framework\TestCase */ private $orderCollectionFactoryMock; + /** + * @var InvoiceProcessItemInterfaceFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $invoiceProcessItemFactoryMock; + /** * @var Transaction|PHPUnit_Framework_MockObject_MockObject */ private $transactionMock; /** - * @var InvoiceService|PHPUnit_Framework_MockObject_MockObject + * @var InvoiceServiceFactory|PHPUnit_Framework_MockObject_MockObject */ - private $invoiceServiceMock; + private $invoiceServiceFactoryMock; /** * @var InvoiceProcess @@ -50,19 +58,26 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->invoiceProcessItemFactoryMock = $this->getMockBuilder(InvoiceProcessItemInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->transactionMock = $this->getMockBuilder(Transaction::class) ->disableOriginalConstructor() ->getMock(); - $this->invoiceServiceMock = $this->getMockBuilder(InvoiceService::class) + $this->invoiceServiceFactoryMock = $this->getMockBuilder(InvoiceServiceFactory::class) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); $this->invoiceProcess = new InvoiceProcess( $this->helperDataMock, $this->orderCollectionFactoryMock, + $this->invoiceProcessItemFactoryMock, $this->transactionMock, - $this->invoiceServiceMock + $this->invoiceServiceFactoryMock ); } @@ -78,10 +93,20 @@ public function testServiceContract() } /** - * @covers \Aune\AutoInvoice\Model\InvoiceProcess::getOrdersToInvoice + * @covers \Aune\AutoInvoice\Model\InvoiceProcess::getItemsToProcess */ - public function testGetOrdersToInvoice() + public function testGetItemsToProcess() { + $dstStatus = 'complete'; + + $this->helperDataMock->expects(self::once()) + ->method('getProcessingRules') + ->willReturn([[ + HelperData::RULE_SOURCE_STATUS => 'processing', + HelperData::RULE_PAYMENT_METHOD => HelperData::RULE_PAYMENT_METHOD_ALL, + HelperData::RULE_DESTINATION_STATUS => $dstStatus, + ]]); + $orderCollectionMock = $this->getMockBuilder(OrderCollection::class) ->disableOriginalConstructor() ->getMock(); @@ -94,31 +119,212 @@ public function testGetOrdersToInvoice() ->method('addFieldToFilter') ->willReturn($orderCollectionMock); + $orders = [ + $this->getOrderMock(1, 'paypal'), + $this->getOrderMock(2, 'paypal_express'), + $this->getOrderMock(3, 'braintree'), + $this->getOrderMock(4, 'braintree'), + $this->getOrderMock(5, 'aune_stripe'), + ]; + + $orderCollectionMock->expects(self::once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator($orders)); + + $items = []; + foreach ($orders as $order) { + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + + $itemMock->expects(self::once()) + ->method('setOrder') + ->with($order) + ->willReturn($itemMock); + + $itemMock->expects(self::once()) + ->method('setDestinationStatus') + ->with($dstStatus) + ->willReturn($itemMock); + + $items[$order->getId()] = $itemMock; + } + + $this->invoiceProcessItemFactoryMock->expects(self::exactly(count($items))) + ->method('create') + ->willReturnOnConsecutiveCalls(...$items); + + $this->assertEquals( + $this->invoiceProcess->getItemsToProcess(), + $items + ); + } + + /** + * @covers \Aune\AutoInvoice\Model\InvoiceProcess::getItemsToProcess + */ + public function testGetItemsToProcessPaymentMethods() + { + $srcStatus = 'processing'; + $dstStatusPaypal = 'complete'; + $dstStatusBraintree = 'processing'; + + $this->helperDataMock->expects(self::once()) + ->method('getProcessingRules') + ->willReturn([[ + HelperData::RULE_SOURCE_STATUS => $srcStatus, + HelperData::RULE_PAYMENT_METHOD => 'paypal', + HelperData::RULE_DESTINATION_STATUS => $dstStatusPaypal, + ], [ + HelperData::RULE_SOURCE_STATUS => $srcStatus, + HelperData::RULE_PAYMENT_METHOD => 'braintree', + HelperData::RULE_DESTINATION_STATUS => $dstStatusBraintree, + ]]); + + $paypalOrders = [ + $this->getOrderMock(1, 'paypal'), + ]; + $braintreeOrders = [ + $this->getOrderMock(3, 'braintree'), + $this->getOrderMock(4, 'braintree') + ]; + $otherOrders = [ + $this->getOrderMock(2, 'paypal_express'), + $this->getOrderMock(5, 'aune_stripe'), + ]; + + $data = [ + $dstStatusPaypal => $paypalOrders, + $dstStatusBraintree => $braintreeOrders, + ]; + + $items = []; + $orderCollectionMocks = []; + + foreach ($data as $dstStatus => $orders) { + $orderCollectionMock = $this->getMockBuilder(OrderCollection::class) + ->disableOriginalConstructor() + ->getMock(); + + $orderCollectionMock->expects(self::exactly(2)) + ->method('addFieldToFilter') + ->withConsecutive( + ['status', ['eq' => $srcStatus]], + ['total_invoiced', ['null' => true]] + ) + ->willReturnOnConsecutiveCalls($orderCollectionMock, $orderCollectionMock); + + $orderCollectionMock->expects(self::once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator(array_merge($orders, $otherOrders))); + + $orderCollectionMocks []= $orderCollectionMock; + + foreach ($orders as $order) { + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + $itemMock->expects(self::once()) + ->method('setOrder') + ->with($order) + ->willReturn($itemMock); + + $itemMock->expects(self::once()) + ->method('setDestinationStatus') + ->with($dstStatus) + ->willReturn($itemMock); + + $items[$order->getId()] = $itemMock; + } + } + + $this->orderCollectionFactoryMock->expects(self::exactly(count($orderCollectionMocks))) + ->method('create') + ->willReturnOnConsecutiveCalls(...$orderCollectionMocks); + + $this->invoiceProcessItemFactoryMock->expects(self::exactly(count($items))) + ->method('create') + ->willReturnOnConsecutiveCalls(...$items); + $this->assertEquals( - $this->invoiceProcess->getOrdersToInvoice(), - $orderCollectionMock + $this->invoiceProcess->getItemsToProcess(), + $items ); } + /** + * Returns new mock order with given payment method + */ + private function getOrderMock(int $id, string $paymentMethod) + { + $methodInstanceMock = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); + + $methodInstanceMock->expects(self::any()) + ->method('getCode') + ->willReturn($paymentMethod); + + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) + ->disableOriginalConstructor() + ->getMock(); + + $paymentMock->expects(self::any()) + ->method('getMethodInstance') + ->willReturn($methodInstanceMock); + + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $orderMock->expects(self::any()) + ->method('getId') + ->willReturn($id); + + $orderMock->expects(self::any()) + ->method('getPayment') + ->willReturn($paymentMock); + + return $orderMock; + } + /** * @covers \Aune\AutoInvoice\Model\InvoiceProcess::invoice */ public function testInvoice() { + $status = 'complete'; $orderMock = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() ->getMock(); + $orderMock->expects(self::once()) + ->method('setStatus') + ->with($status) + ->willReturn($orderMock); + + $itemMock = $this->getMockForAbstractClass(InvoiceProcessItemInterface::class); + + $itemMock->expects(self::once()) + ->method('getOrder') + ->willReturn($orderMock); + + $itemMock->expects(self::once()) + ->method('getDestinationStatus') + ->willReturn($status); + $invoiceMock = $this->getMockBuilder(OrderInvoice::class) ->disableOriginalConstructor() ->setMethods(['setRequestedCaptureCase', 'register']) ->getMock(); - $this->invoiceServiceMock->expects(self::once()) + $invoiceServiceMock = $this->getMockBuilder(InvoiceService::class) + ->disableOriginalConstructor() + ->getMock(); + + $invoiceServiceMock->expects(self::once()) ->method('prepareInvoice') ->with($orderMock) ->willReturn($invoiceMock); + $this->invoiceServiceFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($invoiceServiceMock); + $invoiceMock->expects(self::once()) ->method('setRequestedCaptureCase') ->with(OrderInvoice::CAPTURE_OFFLINE); @@ -133,6 +339,6 @@ public function testInvoice() $this->transactionMock->expects(self::once()) ->method('save'); - $this->invoiceProcess->invoice($orderMock); + $this->invoiceProcess->invoice($itemMock); } } diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 60a59c1..b423796 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -9,9 +9,10 @@ Automatically invoice orders every hour Magento\Config\Model\Config\Source\Yesno - - - Aune\AutoInvoice\Model\Config\Source\Order\Status + + + Aune\AutoInvoice\Block\Adminhtml\Form\Field\ProcessingRule + Aune\AutoInvoice\Model\Adminhtml\System\Config\ProcessingRule diff --git a/etc/di.xml b/etc/di.xml index 19fa524..a9435fc 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -4,6 +4,9 @@ + +