diff --git a/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js b/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js
deleted file mode 100644
index f46ad4192d170..0000000000000
--- a/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
-
-define([
- 'jquery',
- 'Magento_Ui/js/modal/modal',
- 'mage/translate'
-], function ($, modal, $t) {
- 'use strict';
-
- return function (config, element) {
- config.buttons = [
- {
- text: $t('Print'),
- 'class': 'action action-primary',
-
- /**
- * Click handler
- */
- click: function () {
- window.location.href = this.options.url;
- }
- }, {
- text: $t('Cancel'),
- 'class': 'action action-secondary',
-
- /**
- * Click handler
- */
- click: function () {
- this.closeModal();
- }
- }
- ];
- modal(config, element);
- };
-});
diff --git a/app/code/Magento/Signifyd/Api/CaseCreationServiceInterface.php b/app/code/Magento/Signifyd/Api/CaseCreationServiceInterface.php
new file mode 100644
index 0000000000000..f7611660b93a1
--- /dev/null
+++ b/app/code/Magento/Signifyd/Api/CaseCreationServiceInterface.php
@@ -0,0 +1,29 @@
+caseManagement = $caseManagement;
+
+ parent::__construct($context, $data);
+ }
+
+ /**
+ * Gets case entity associated with order id.
+ *
+ * @return CaseInterface|null
+ */
+ private function getCaseEntity()
+ {
+ if ($this->caseEntity === false) {
+ $this->caseEntity = $this->caseManagement->getByOrderId(
+ $this->getOrderId()
+ );
+ }
+
+ return $this->caseEntity;
+ }
+
+ /**
+ * Default getter for case properties
+ *
+ * @param mixed $defaultValue
+ * @param callable $callback
+ * @return mixed
+ */
+ private function getCaseProperty($defaultValue, callable $callback)
+ {
+ return $this->isEmptyCase() ? $defaultValue : call_user_func($callback);
+ }
+
+ /**
+ * Checks if case is exists for order
+ *
+ * @return bool
+ * @since 100.2.0
+ */
+ public function isEmptyCase()
+ {
+ return $this->getCaseEntity() === null;
+ }
+
+ /**
+ * Gets case guarantee disposition status.
+ *
+ * @return string
+ * @since 100.2.0
+ */
+ public function getCaseGuaranteeDisposition()
+ {
+ return $this->getCaseProperty('', function () {
+ $guaranteeStatusMap = [
+ CaseInterface::GUARANTEE_APPROVED => __('Approved'),
+ CaseInterface::GUARANTEE_DECLINED => __('Declined'),
+ CaseInterface::GUARANTEE_PENDING => __('Pending'),
+ CaseInterface::GUARANTEE_CANCELED => __('Canceled'),
+ CaseInterface::GUARANTEE_IN_REVIEW => __('In Review'),
+ CaseInterface::GUARANTEE_UNREQUESTED => __('Unrequested')
+ ];
+
+ $status = isset($guaranteeStatusMap[$this->getCaseEntity()->getGuaranteeDisposition()]) ?
+ $guaranteeStatusMap[$this->getCaseEntity()->getGuaranteeDisposition()] :
+ '';
+
+ return $status;
+ });
+ }
+
+ /**
+ * Retrieves current order Id.
+ *
+ * @return integer
+ */
+ private function getOrderId()
+ {
+ return (int) $this->getRequest()->getParam('order_id');
+ }
+}
diff --git a/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Field/WebhookUrl.php b/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Field/WebhookUrl.php
new file mode 100644
index 0000000000000..7964d6b1af397
--- /dev/null
+++ b/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Field/WebhookUrl.php
@@ -0,0 +1,60 @@
+getOriginalData();
+ if (!empty($originalData['handler_url'])) {
+ $url = $this->getStoreUrl();
+ $url .= $originalData['handler_url'];
+ }
+
+ return '
' . $this->escapeHtml($url) . '
';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function _isInheritCheckboxRequired(AbstractElement $element)
+ {
+ return false;
+ }
+
+ /**
+ * Return base store URL.
+ *
+ * @return string
+ */
+ private function getStoreUrl()
+ {
+ $website = $this->_storeManager->getWebsite($this->getRequest()->getParam('website'));
+
+ $isSecure = $this->_scopeConfig->isSetFlag(
+ Store::XML_PATH_SECURE_IN_FRONTEND,
+ ScopeInterface::SCOPE_WEBSITE,
+ $website->getCode()
+ );
+
+ $configPath = $isSecure ? Store::XML_PATH_SECURE_BASE_LINK_URL : Store::XML_PATH_UNSECURE_BASE_LINK_URL;
+
+ return $this->_scopeConfig->getValue($configPath, ScopeInterface::SCOPE_WEBSITE, $website->getCode());
+ }
+}
diff --git a/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Fieldset/Info.php b/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Fieldset/Info.php
new file mode 100644
index 0000000000000..c18c3dc596e21
--- /dev/null
+++ b/app/code/Magento/Signifyd/Block/Adminhtml/System/Config/Fieldset/Info.php
@@ -0,0 +1,31 @@
+getGroup();
+
+ if (!empty($groupConfig['more_url']) && !empty($element->getComment())) {
+ $comment = $element->getComment();
+ $comment .= '' .
+ $this->escapeHtml(__('Learn more')) . '
';
+ $element->setComment($comment);
+ }
+
+ return parent::_getHeaderCommentHtml($element);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Block/Fingerprint.php b/app/code/Magento/Signifyd/Block/Fingerprint.php
new file mode 100644
index 0000000000000..7afa092b3d0da
--- /dev/null
+++ b/app/code/Magento/Signifyd/Block/Fingerprint.php
@@ -0,0 +1,90 @@
+signifydOrderSessionId = $signifydOrderSessionId;
+ $this->config = $config;
+ $this->quoteSession = $quoteSession;
+ }
+
+ /**
+ * Returns a unique Signifyd order session id.
+ *
+ * @return string
+ * @since 100.2.0
+ */
+ public function getSignifydOrderSessionId()
+ {
+ $quoteId = $this->quoteSession->getQuote()->getId();
+
+ return $this->signifydOrderSessionId->get($quoteId);
+ }
+
+ /**
+ * Checks if module is enabled.
+ *
+ * @return boolean
+ * @since 100.2.0
+ */
+ public function isModuleActive()
+ {
+ return $this->config->isActive();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Controller/Webhooks/Handler.php b/app/code/Magento/Signifyd/Controller/Webhooks/Handler.php
new file mode 100644
index 0000000000000..12bd773d35a2f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Controller/Webhooks/Handler.php
@@ -0,0 +1,139 @@
+webhookRequest = $webhookRequest;
+ $this->logger = $logger;
+ $this->webhookMessageReader = $webhookMessageReader;
+ $this->caseUpdatingServiceFactory = $caseUpdatingServiceFactory;
+ $this->webhookRequestValidator = $webhookRequestValidator;
+ $this->caseRepository = $caseRepository;
+ $this->config = $config;
+ }
+
+ /**
+ * Processes webhook request data and updates case entity
+ *
+ * @return void
+ */
+ public function execute()
+ {
+ if ($this->config->isDebugModeEnabled()) {
+ $this->logger->debug($this->webhookRequest->getEventTopic() . '|' . $this->webhookRequest->getBody());
+ }
+
+ if (!$this->webhookRequestValidator->validate($this->webhookRequest)) {
+ $this->_redirect('noroute');
+ return;
+ }
+
+ $webhookMessage = $this->webhookMessageReader->read($this->webhookRequest);
+ if ($webhookMessage->getEventTopic() === self::$eventTopicTest) {
+ return;
+ }
+
+ $data = $webhookMessage->getData();
+ if (empty($data['caseId'])) {
+ $this->_redirect('noroute');
+ return;
+ }
+
+ $case = $this->caseRepository->getByCaseId($data['caseId']);
+ if ($case === null) {
+ $this->_redirect('noroute');
+ return;
+ }
+
+ $caseUpdatingService = $this->caseUpdatingServiceFactory->create($webhookMessage->getEventTopic());
+ try {
+ $caseUpdatingService->update($case, $data);
+ } catch (LocalizedException $e) {
+ $this->getResponse()->setHttpResponseCode(400);
+ $this->logger->critical($e);
+ }
+ }
+}
diff --git a/app/code/Magento/Signifyd/LICENSE.txt b/app/code/Magento/Signifyd/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Signifyd/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/Signifyd/LICENSE_AFL.txt b/app/code/Magento/Signifyd/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Signifyd/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/Signifyd/Model/CaseEntity.php b/app/code/Magento/Signifyd/Model/CaseEntity.php
new file mode 100644
index 0000000000000..c11c72db79f16
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseEntity.php
@@ -0,0 +1,249 @@
+serializer = $serializer;
+ parent::__construct($context, $registry, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function _construct()
+ {
+ $this->_init(ResourceModel\CaseEntity::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getEntityId()
+ {
+ return (int) $this->getData('entity_id');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setEntityId($id)
+ {
+ $this->setData('entity_id', (int) $id);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCaseId()
+ {
+ return (int) $this->getData('case_id');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setCaseId($id)
+ {
+ $this->setData('case_id', (int) $id);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isGuaranteeEligible()
+ {
+ $value = $this->getData('guarantee_eligible');
+ return ($value === null) ? $value : (bool) $value;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setGuaranteeEligible($guaranteeEligible)
+ {
+ $this->setData('guarantee_eligible', $guaranteeEligible);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getGuaranteeDisposition()
+ {
+ return (string) $this->getData('guarantee_disposition');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setGuaranteeDisposition($disposition)
+ {
+ $this->setData('guarantee_disposition', (string) $disposition);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getStatus()
+ {
+ return (string) $this->getData('status');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setStatus($status)
+ {
+ $this->setData('status', (string) $status);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getScore()
+ {
+ return (int) $this->getData('score');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setScore($score)
+ {
+ $this->setData('score', (int) $score);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getOrderId()
+ {
+ return (int) $this->getData('order_id');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setOrderId($orderId)
+ {
+ $this->setData('order_id', (int) $orderId);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAssociatedTeam()
+ {
+ $teamData = $this->getData('associated_team');
+ return empty($teamData) ? [] : $this->serializer->unserialize($teamData);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setAssociatedTeam(array $team)
+ {
+ $this->setData('associated_team', $this->serializer->serialize($team));
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getReviewDisposition()
+ {
+ return (string) $this->getData('review_disposition');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setReviewDisposition($disposition)
+ {
+ $this->setData('review_disposition', (string) $disposition);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCreatedAt()
+ {
+ return $this->getData('created_at');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setCreatedAt($datetime)
+ {
+ $this->setData('created_at', $datetime);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getUpdatedAt()
+ {
+ return $this->getData('updated_at');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setUpdatedAt($datetime)
+ {
+ $this->setData('updated_at', $datetime);
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseManagement.php b/app/code/Magento/Signifyd/Model/CaseManagement.php
new file mode 100644
index 0000000000000..1913f1e7a17b3
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseManagement.php
@@ -0,0 +1,93 @@
+caseRepository = $caseRepository;
+ $this->caseFactory = $caseFactory;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->filterBuilder = $filterBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function create($orderId)
+ {
+ /** @var \Magento\Signifyd\Api\Data\CaseInterface $case */
+ $case = $this->caseFactory->create();
+ $case->setOrderId($orderId)
+ ->setStatus(CaseInterface::STATUS_PENDING)
+ ->setGuaranteeDisposition(CaseInterface::GUARANTEE_PENDING);
+ try {
+ return $this->caseRepository->save($case);
+ } catch (DuplicateException $e) {
+ throw new AlreadyExistsException(__('This order already has associated case entity'), $e);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getByOrderId($orderId)
+ {
+ $filters = [
+ $this->filterBuilder->setField('order_id')
+ ->setValue($orderId)
+ ->create()
+ ];
+ $searchCriteria = $this->searchCriteriaBuilder->addFilters($filters)->create();
+ $items = $this->caseRepository->getList($searchCriteria)->getItems();
+ return !empty($items) ? array_pop($items) : null;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseRepository.php b/app/code/Magento/Signifyd/Model/CaseRepository.php
new file mode 100644
index 0000000000000..ea3ea3e67aafd
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseRepository.php
@@ -0,0 +1,134 @@
+collectionProcessor = $collectionProcessor;
+ $this->collectionFactory = $collectionFactory;
+ $this->searchResultsFactory = $searchResultsFactory;
+ $this->caseFactory = $caseFactory;
+ $this->resourceModel = $resourceModel;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function save(CaseInterface $case)
+ {
+ /** @var CaseEntity $case */
+ $this->resourceModel->save($case);
+
+ return $case;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getById($id)
+ {
+ /** @var CaseEntity $case */
+ $case = $this->caseFactory->create();
+ $this->resourceModel->load($case, $id);
+
+ return $case;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getByCaseId($caseId)
+ {
+ /** @var CaseEntity $case */
+ $case = $this->caseFactory->create();
+ $this->resourceModel->load($case, $caseId, 'case_id');
+
+ return $case->getEntityId() ? $case : null;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete(CaseInterface $case)
+ {
+ $this->resourceModel->delete($case);
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getList(SearchCriteriaInterface $searchCriteria)
+ {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $this->collectionProcessor->process($searchCriteria, $collection);
+
+ /** @var CaseSearchResultsInterface $searchResults */
+ $searchResults = $this->searchResultsFactory->create();
+ $searchResults->setSearchCriteria($searchCriteria);
+ $searchResults->setItems($collection->getItems());
+
+ return $searchResults;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseServices/CreationService.php b/app/code/Magento/Signifyd/Model/CaseServices/CreationService.php
new file mode 100644
index 0000000000000..8413838bd7d5f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseServices/CreationService.php
@@ -0,0 +1,101 @@
+caseManagement = $caseManagement;
+ $this->signifydGateway = $signifydGateway;
+ $this->logger = $logger;
+ $this->caseRepository = $caseRepository;
+ $this->orderGridUpdater = $orderGridUpdater;
+ $this->orderStateService = $orderStateService;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createForOrder($orderId)
+ {
+ $case = $this->caseManagement->create($orderId);
+ $this->orderGridUpdater->update($orderId);
+
+ try {
+ $caseId = $this->signifydGateway->createCase($orderId);
+ } catch (GatewayException $e) {
+ $this->logger->error($e->getMessage());
+ return true;
+ }
+
+ $case->setCaseId($caseId);
+ $this->caseRepository->save($case);
+ $this->orderStateService->updateByCase($case);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseServices/StubUpdatingService.php b/app/code/Magento/Signifyd/Model/CaseServices/StubUpdatingService.php
new file mode 100644
index 0000000000000..295d7f13fb0ac
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseServices/StubUpdatingService.php
@@ -0,0 +1,23 @@
+messageGenerator = $messageGenerator;
+ $this->caseRepository = $caseRepository;
+ $this->commentsHistoryUpdater = $commentsHistoryUpdater;
+ $this->orderGridUpdater = $orderGridUpdater;
+ $this->orderStateService = $orderStateService;
+ }
+
+ /**
+ * Updates Signifyd Case entity by received data.
+ *
+ * @param CaseInterface $case
+ * @param array $data
+ * @return void
+ * @throws NotFoundException
+ * @throws LocalizedException
+ */
+ public function update(CaseInterface $case, array $data)
+ {
+ if (empty($case->getEntityId()) || empty($case->getCaseId())) {
+ throw new LocalizedException(__('The case entity should not be empty.'));
+ }
+
+ try {
+ $previousDisposition = $case->getGuaranteeDisposition();
+ $this->setCaseData($case, $data);
+ $orderHistoryComment = $this->messageGenerator->generate($data);
+ $case = $this->caseRepository->save($case);
+ $this->orderGridUpdater->update($case->getOrderId());
+ $this->commentsHistoryUpdater->addComment($case, $orderHistoryComment);
+ if ($case->getGuaranteeDisposition() !== $previousDisposition) {
+ $this->orderStateService->updateByCase($case);
+ }
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('Cannot update Case entity.'), $e);
+ }
+ }
+
+ /**
+ * Sets data to case entity.
+ *
+ * @param CaseInterface $case
+ * @param array $data
+ * @return void
+ */
+ private function setCaseData(CaseInterface $case, array $data)
+ {
+ // list of keys which should not be replaced, like order id
+ $notResolvedKeys = [
+ 'orderId'
+ ];
+ foreach ($data as $key => $value) {
+ $methodName = 'set' . ucfirst($key);
+ if (!in_array($key, $notResolvedKeys) && method_exists($case, $methodName)) {
+ call_user_func([$case, $methodName], $value);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceFactory.php b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceFactory.php
new file mode 100644
index 0000000000000..5415044b5edc4
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceFactory.php
@@ -0,0 +1,78 @@
+objectManager = $objectManager;
+ $this->generatorFactory = $generatorFactory;
+ $this->config = $config;
+ }
+
+ /**
+ * Creates instance of service updating case.
+ * As param retrieves type of message generator.
+ *
+ * @param string $type
+ * @return UpdatingServiceInterface
+ * @throws \InvalidArgumentException
+ */
+ public function create($type)
+ {
+ if (!$this->config->isActive() || $type === self::$caseTest) {
+ return $this->objectManager->create(StubUpdatingService::class);
+ }
+
+ $messageGenerator = $this->generatorFactory->create($type);
+ $service = $this->objectManager->create(UpdatingService::class, [
+ 'messageGenerator' => $messageGenerator
+ ]);
+
+ return $service;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceInterface.php b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceInterface.php
new file mode 100644
index 0000000000000..daa7b40bfd674
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingServiceInterface.php
@@ -0,0 +1,23 @@
+historyFactory = $historyFactory;
+ $this->historyRepository = $historyRepository;
+ }
+
+ /**
+ * Adds comment to case related order.
+ * Throws an exception if cannot save history comment.
+ *
+ * @param CaseInterface $case
+ * @param Phrase $message
+ * @param string $status
+ * @return void
+ */
+ public function addComment(CaseInterface $case, Phrase $message, $status = '')
+ {
+ if (!$message->getText()) {
+ return;
+ }
+
+ /** @var \Magento\Sales\Api\Data\OrderStatusHistoryInterface $history */
+ $history = $this->historyFactory->create();
+ $history->setParentId($case->getOrderId())
+ ->setComment($message)
+ ->setEntityName('order')
+ ->setStatus($status);
+ $this->historyRepository->save($history);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/Config.php b/app/code/Magento/Signifyd/Model/Config.php
new file mode 100644
index 0000000000000..b68380ee15bf3
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/Config.php
@@ -0,0 +1,93 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * If this config option set to false no Signifyd integration should be available
+ * (only possibility to configure Signifyd setting in admin)
+ *
+ * @return bool
+ */
+ public function isActive()
+ {
+ $enabled = $this->scopeConfig->isSetFlag(
+ 'fraud_protection/signifyd/active',
+ ScopeInterface::SCOPE_STORE
+ );
+ return $enabled;
+ }
+
+ /**
+ * Signifyd API Key used for authentication.
+ *
+ * @see https://www.signifyd.com/docs/api/#/introduction/authentication
+ * @see https://app.signifyd.com/settings
+ *
+ * @return string
+ */
+ public function getApiKey()
+ {
+ $apiKey = $this->scopeConfig->getValue(
+ 'fraud_protection/signifyd/api_key',
+ ScopeInterface::SCOPE_STORE
+ );
+ return $apiKey;
+ }
+
+ /**
+ * Base URL to Signifyd REST API.
+ * Usually equals to https://api.signifyd.com/v2 and should not be changed
+ *
+ * @return string
+ */
+ public function getApiUrl()
+ {
+ $apiUrl = $this->scopeConfig->getValue(
+ 'fraud_protection/signifyd/api_url',
+ ScopeInterface::SCOPE_STORE
+ );
+ return $apiUrl;
+ }
+
+ /**
+ * If is "true" extra information about interaction with Signifyd API are written to debug.log file
+ *
+ * @return bool
+ */
+ public function isDebugModeEnabled()
+ {
+ $debugModeEnabled = $this->scopeConfig->isSetFlag(
+ 'fraud_protection/signifyd/debug',
+ ScopeInterface::SCOPE_STORE
+ );
+ return $debugModeEnabled;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/CustomerOrders.php b/app/code/Magento/Signifyd/Model/CustomerOrders.php
new file mode 100644
index 0000000000000..c326cf06424c0
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/CustomerOrders.php
@@ -0,0 +1,173 @@
+searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->filterBuilder = $filterBuilder;
+ $this->orderRepository = $orderRepository;
+ $this->currencyFactory = $currencyFactory;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Returns aggregated customer orders count and total amount in USD.
+ *
+ * Returned array contains next keys:
+ * aggregateOrderCount - total count of orders placed by this account since it was created, including the current
+ * aggregateOrderDollars - total amount spent by this account since it was created, including the current order
+ *
+ * @param int $customerId
+ * @return array
+ */
+ public function getAggregatedOrdersInfo($customerId)
+ {
+ $result = [
+ 'aggregateOrderCount' => null,
+ 'aggregateOrderDollars' => null
+ ];
+
+ $customerOrders = $this->getCustomerOrders($customerId);
+ if (!empty($customerOrders)) {
+ try {
+ $orderTotalDollars = 0.0;
+ foreach ($customerOrders as $order) {
+ $orderTotalDollars += $this->getUsdOrderTotal(
+ $order->getBaseGrandTotal(),
+ $order->getBaseCurrencyCode()
+ );
+ }
+ $result = [
+ 'aggregateOrderCount' => count($customerOrders),
+ 'aggregateOrderDollars' => $orderTotalDollars
+ ];
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns customer orders.
+ *
+ * @param int $customerId
+ * @return OrderInterface[]
+ */
+ private function getCustomerOrders($customerId)
+ {
+ $filters = [
+ $this->filterBuilder->setField(OrderInterface::CUSTOMER_ID)->setValue($customerId)->create()
+ ];
+ $this->searchCriteriaBuilder->addFilters($filters);
+ $searchCriteria = $this->searchCriteriaBuilder->create();
+ $searchResults = $this->orderRepository->getList($searchCriteria);
+
+ return $searchResults->getItems();
+ }
+
+ /**
+ * Returns amount in USD.
+ *
+ * @param float $amount
+ * @param string $currency
+ * @return float
+ */
+ private function getUsdOrderTotal($amount, $currency)
+ {
+ if ($currency === self::$usdCurrencyCode) {
+ return $amount;
+ }
+
+ $operationCurrency = $this->getCurrencyByCode($currency);
+
+ return $operationCurrency->convert($amount, self::$usdCurrencyCode);
+ }
+
+ /**
+ * Returns currency by currency code.
+ *
+ * @param string|null $currencyCode
+ * @return Currency
+ */
+ private function getCurrencyByCode($currencyCode)
+ {
+ if (isset($this->currencies[$currencyCode])) {
+ return $this->currencies[$currencyCode];
+ }
+
+ /** @var Currency $currency */
+ $currency = $this->currencyFactory->create();
+ $this->currencies[$currencyCode] = $currency->load($currencyCode);
+
+ return $this->currencies[$currencyCode];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/Guarantee/CancelGuaranteeAbility.php b/app/code/Magento/Signifyd/Model/Guarantee/CancelGuaranteeAbility.php
new file mode 100644
index 0000000000000..65bede6f0cea5
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/Guarantee/CancelGuaranteeAbility.php
@@ -0,0 +1,84 @@
+caseManagement = $caseManagement;
+ $this->orderRepository = $orderRepository;
+ }
+
+ /**
+ * Checks if it is possible to create Guarantee for order and case.
+ *
+ * @param int $orderId
+ * @return bool
+ */
+ public function isAvailable($orderId)
+ {
+ $case = $this->caseManagement->getByOrderId($orderId);
+ if ($case === null) {
+ return false;
+ }
+
+ if (in_array($case->getGuaranteeDisposition(), [null, $case::GUARANTEE_CANCELED])) {
+ return false;
+ }
+
+ $order = $this->getOrder($orderId);
+ if (null === $order) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns order by id
+ *
+ * @param int $orderId
+ * @return OrderInterface|null
+ */
+ private function getOrder($orderId)
+ {
+ try {
+ $order = $this->orderRepository->get($orderId);
+ } catch (InputException $e) {
+ return null;
+ } catch (NoSuchEntityException $e) {
+ return null;
+ }
+
+ return $order;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/Guarantee/CancelingService.php b/app/code/Magento/Signifyd/Model/Guarantee/CancelingService.php
new file mode 100644
index 0000000000000..b30efac8c2190
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/Guarantee/CancelingService.php
@@ -0,0 +1,94 @@
+caseManagement = $caseManagement;
+ $this->serviceFactory = $serviceFactory;
+ $this->gateway = $gateway;
+ $this->cancelGuaranteeAbility = $cancelGuaranteeAbility;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function cancelForOrder($orderId)
+ {
+ if (!$this->cancelGuaranteeAbility->isAvailable($orderId)) {
+ return false;
+ }
+
+ $caseEntity = $this->caseManagement->getByOrderId($orderId);
+
+ try {
+ $disposition = $this->gateway->cancelGuarantee($caseEntity->getCaseId());
+ } catch (GatewayException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+
+ $updatingService = $this->serviceFactory->create('guarantees/cancel');
+ $data = [
+ 'guaranteeDisposition' => $disposition
+ ];
+ $updatingService->update($caseEntity, $data);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/Guarantee/CreateGuaranteeAbility.php b/app/code/Magento/Signifyd/Model/Guarantee/CreateGuaranteeAbility.php
new file mode 100644
index 0000000000000..15addba3ec4fd
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/Guarantee/CreateGuaranteeAbility.php
@@ -0,0 +1,124 @@
+caseManagement = $caseManagement;
+ $this->orderRepository = $orderRepository;
+ $this->dateTimeFactory = $dateTimeFactory;
+ }
+
+ /**
+ * Checks if it is possible to create Guarantee for order and case.
+ *
+ * @param int $orderId
+ * @return bool
+ */
+ public function isAvailable($orderId)
+ {
+ $case = $this->caseManagement->getByOrderId($orderId);
+ if (null === $case) {
+ return false;
+ }
+
+ if ($case->isGuaranteeEligible() === false) {
+ return false;
+ }
+
+ $order = $this->getOrder($orderId);
+ if (null === $order) {
+ return false;
+ }
+
+ if (in_array($order->getState(), [Order::STATE_CANCELED, Order::STATE_CLOSED])) {
+ return false;
+ }
+
+ if ($this->isOrderOlderThen(static::$guarantyEligibleDays, $order)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if Guarantee submit is applicable for order.
+ *
+ * @param OrderInterface $order
+ * @param int $days number of days from the order creation date to submit a case for Guarantee.
+ * @return bool
+ */
+ private function isOrderOlderThen($days, OrderInterface $order)
+ {
+ $orderCreateDate = $this->dateTimeFactory->create($order->getCreatedAt(), new \DateTimeZone('UTC'));
+ $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
+
+ return $orderCreateDate->diff($currentDate)->days >= $days;
+ }
+
+ /**
+ * Returns order by id
+ *
+ * @param int $orderId
+ * @return OrderInterface|null
+ */
+ private function getOrder($orderId)
+ {
+ try {
+ $order = $this->orderRepository->get($orderId);
+ } catch (InputException $e) {
+ return null;
+ } catch (NoSuchEntityException $e) {
+ return null;
+ }
+
+ return $order;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/Guarantee/CreationService.php b/app/code/Magento/Signifyd/Model/Guarantee/CreationService.php
new file mode 100644
index 0000000000000..4080aee453f18
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/Guarantee/CreationService.php
@@ -0,0 +1,97 @@
+caseManagement = $caseManagement;
+ $this->caseUpdatingServiceFactory = $caseUpdatingServiceFactory;
+ $this->gateway = $gateway;
+ $this->createGuaranteeAbility = $createGuaranteeAbility;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function createForOrder($orderId)
+ {
+ if (!$this->createGuaranteeAbility->isAvailable($orderId)) {
+ return false;
+ }
+
+ $caseEntity = $this->caseManagement->getByOrderId($orderId);
+
+ try {
+ $disposition = $this->gateway->submitCaseForGuarantee($caseEntity->getCaseId());
+ } catch (GatewayException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+
+ $updatingService = $this->caseUpdatingServiceFactory->create('guarantees/creation');
+ $data = [
+ 'caseId' => $caseEntity->getCaseId(),
+ 'guaranteeDisposition' => $disposition
+ ];
+ $updatingService->update($caseEntity, $data);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/MessageGenerators/CaseRescore.php b/app/code/Magento/Signifyd/Model/MessageGenerators/CaseRescore.php
new file mode 100644
index 0000000000000..d0e89854e3909
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/MessageGenerators/CaseRescore.php
@@ -0,0 +1,51 @@
+caseRepository = $caseRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function generate(array $data)
+ {
+ if (empty($data['caseId'])) {
+ throw new GeneratorException(__('The "%1" should not be empty.', 'caseId'));
+ }
+
+ $caseEntity = $this->caseRepository->getByCaseId($data['caseId']);
+
+ if ($caseEntity === null) {
+ throw new GeneratorException(__('Case entity not found.'));
+ }
+
+ return __(
+ 'Case Update: New score for the order is %1. Previous score was %2.',
+ !empty($data['score']) ? $data['score'] : 0,
+ $caseEntity->getScore()
+ );
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorException.php b/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorException.php
new file mode 100644
index 0000000000000..103cb9fc1e2d3
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorException.php
@@ -0,0 +1,19 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Creates instance of message generator.
+ * Throws exception if type of message generator does not have implementations.
+ *
+ * @param string $type
+ * @return GeneratorInterface
+ * @throws \InvalidArgumentException
+ */
+ public function create($type)
+ {
+ $className = PatternGenerator::class;
+ switch ($type) {
+ case self::$caseCreation:
+ $classConfig = [
+ 'template' => 'Signifyd Case %1 has been created for order.',
+ 'requiredParams' => ['caseId']
+ ];
+ break;
+ case self::$caseRescore:
+ $classConfig = [];
+ $className = CaseRescore::class;
+ break;
+ case self::$caseReview:
+ $classConfig = [
+ 'template' => 'Case Update: Case Review was completed. Review Deposition is %1.',
+ 'requiredParams' => ['reviewDisposition']
+ ];
+ break;
+ case self::$guaranteeCompletion:
+ $classConfig = [
+ 'template' => 'Case Update: Guarantee Disposition is %1.',
+ 'requiredParams' => ['guaranteeDisposition']
+ ];
+ break;
+ case self::$guaranteeCreation:
+ $classConfig = [
+ 'template' => 'Case Update: Case is submitted for guarantee.'
+ ];
+ break;
+ case self::$guaranteeCancel:
+ $classConfig = [
+ 'template' => 'Case Update: Case guarantee has been cancelled.'
+ ];
+ break;
+ default:
+ throw new \InvalidArgumentException('Specified message type does not supported.');
+ }
+
+ return $this->objectManager->create($className, $classConfig);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorInterface.php b/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorInterface.php
new file mode 100644
index 0000000000000..385cbe35f05ac
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/MessageGenerators/GeneratorInterface.php
@@ -0,0 +1,21 @@
+template = $template;
+ $this->requiredParams = $requiredParams;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function generate(array $data)
+ {
+ $placeholders = [];
+ foreach ($this->requiredParams as $param) {
+ if (empty($data[$param])) {
+ throw new GeneratorException(__('The "%1" should not be empty.', $param));
+ }
+ $placeholders[] = $data[$param];
+ }
+ return __($this->template, ...$placeholders);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/OrderStateService.php b/app/code/Magento/Signifyd/Model/OrderStateService.php
new file mode 100644
index 0000000000000..2b3f0e155981e
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/OrderStateService.php
@@ -0,0 +1,119 @@
+orderFactory = $orderFactory;
+ $this->orderManagement = $orderManagement;
+ $this->commentsHistoryUpdater = $commentsHistoryUpdater;
+ }
+
+ /**
+ * Updates order state depending on case guarantee disposition status.
+ *
+ * @param CaseInterface $case
+ * @return void
+ */
+ public function updateByCase(CaseInterface $case)
+ {
+ $orderId = $case->getOrderId();
+
+ switch ($case->getGuaranteeDisposition()) {
+ case CaseInterface::GUARANTEE_APPROVED:
+ $this->unHold($orderId);
+ break;
+ case CaseInterface::GUARANTEE_DECLINED:
+ $this->hold($orderId);
+ break;
+ case CaseInterface::GUARANTEE_PENDING:
+ if ($this->hold($orderId)) {
+ $this->commentsHistoryUpdater->addComment(
+ $case,
+ __('Awaiting the Signifyd guarantee disposition.'),
+ Order::STATE_HOLDED
+ );
+ }
+ break;
+ }
+ }
+
+ /**
+ * Tries to unhold the order.
+ *
+ * @param int $orderId
+ * @return bool
+ */
+ private function unHold($orderId)
+ {
+ $order = $this->getOrder($orderId);
+ if ($order->canUnhold()) {
+ return $this->orderManagement->unHold($orderId);
+ }
+
+ return false;
+ }
+
+ /**
+ * Tries to hold the order.
+ *
+ * @param int $orderId
+ * @return bool
+ */
+ private function hold($orderId)
+ {
+ $order = $this->getOrder($orderId);
+ if ($order->canHold()) {
+ return $this->orderManagement->hold($orderId);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the order.
+ *
+ * @param int $orderId
+ * @return Order
+ */
+ private function getOrder($orderId)
+ {
+ return $this->orderFactory->create()->load($orderId);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/PaymentMethodMapper/PaymentMethodMapper.php b/app/code/Magento/Signifyd/Model/PaymentMethodMapper/PaymentMethodMapper.php
new file mode 100644
index 0000000000000..cdf9041510b45
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/PaymentMethodMapper/PaymentMethodMapper.php
@@ -0,0 +1,40 @@
+paymentMethodMapping = $paymentMapping;
+ }
+
+ /**
+ * Gets the Sygnifyd payment method by the order's payment method.
+ *
+ * @param string $paymentMethod
+ * @return string
+ */
+ public function getSignifydPaymentMethodCode($paymentMethod)
+ {
+ return $this->paymentMethodMapping->get($paymentMethod, '');
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/PaymentMethodMapper/XmlToArrayConfigConverter.php b/app/code/Magento/Signifyd/Model/PaymentMethodMapper/XmlToArrayConfigConverter.php
new file mode 100644
index 0000000000000..5c3f23bb92729
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/PaymentMethodMapper/XmlToArrayConfigConverter.php
@@ -0,0 +1,88 @@
+getElementsByTagName(self::$paymentMethodNodeType);
+ $paymentsList = [];
+ foreach ($paymentMethods as $paymentMethod) {
+ $paymentsList += $this->getPaymentMethodMapping($paymentMethod);
+ }
+
+ return $paymentsList;
+ }
+
+ /**
+ * Adds a payment method as key and a Sygnifyd payment method as value
+ * in the payment list array
+ *
+ * @param \DOMElement $payment
+ * @return array
+ * @throws ValidationSchemaException
+ */
+ private function getPaymentMethodMapping(\DOMElement $payment)
+ {
+ $paymentMethodCode = $this->readSubnodeValue($payment, self::$magentoCodeNodeType);
+ $signifyPaymentMethodCode = $this->readSubnodeValue($payment, self::$signifydCodeNodeType);
+
+ return [$paymentMethodCode => $signifyPaymentMethodCode];
+ }
+
+ /**
+ * Reads node value by node type
+ *
+ * @param \DOMElement $element
+ * @param string $subNodeType
+ * @return mixed
+ * @throws ValidationSchemaException
+ */
+ private function readSubnodeValue(\DOMElement $element, $subNodeType)
+ {
+ $domList = $element->getElementsByTagName($subNodeType);
+ if (empty($domList[0])) {
+ throw new ValidationSchemaException(__('Only single entrance of "%1" node is required.', $subNodeType));
+ }
+
+ $subNodeValue = trim($domList[0]->nodeValue);
+ if (!$subNodeValue) {
+ throw new ValidationSchemaException(__('Not empty value for "%1" node is required.', $subNodeType));
+ }
+
+ return $subNodeValue;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php
new file mode 100644
index 0000000000000..a26beda520944
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php
@@ -0,0 +1,109 @@
+config = $config;
+ $this->objectManager = $objectManager;
+ $this->avsDefaultAdapter = $avsDefaultAdapter;
+ $this->cvvDefaultAdapter = $cvvDefaultAdapter;
+ }
+
+ /**
+ * Creates instance of CVV code verification.
+ * Exception will be thrown if CVV mapper does not implement PaymentVerificationInterface.
+ *
+ * @param string $paymentCode
+ * @return PaymentVerificationInterface
+ * @throws \Exception
+ */
+ public function createPaymentCvv($paymentCode)
+ {
+ return $this->create($this->cvvDefaultAdapter, $paymentCode, 'cvv_ems_adapter');
+ }
+
+ /**
+ * Creates instance of AVS code verification.
+ * Exception will be thrown if AVS mapper does not implement PaymentVerificationInterface.
+ *
+ * @param string $paymentCode
+ * @return PaymentVerificationInterface
+ * @throws \Exception
+ */
+ public function createPaymentAvs($paymentCode)
+ {
+ return $this->create($this->avsDefaultAdapter, $paymentCode, 'avs_ems_adapter');
+ }
+
+ /**
+ * Creates instance of PaymentVerificationInterface.
+ * Default implementation will be returned if payment method does not implement PaymentVerificationInterface.
+ *
+ * @param PaymentVerificationInterface $defaultAdapter
+ * @param string $paymentCode
+ * @param string $configKey
+ * @return PaymentVerificationInterface
+ * @throws ConfigurationMismatchException If payment verification instance
+ * does not implement PaymentVerificationInterface.
+ */
+ private function create(PaymentVerificationInterface $defaultAdapter, $paymentCode, $configKey)
+ {
+ $this->config->setMethodCode($paymentCode);
+ $verificationClass = $this->config->getValue($configKey);
+ if ($verificationClass === null) {
+ return $defaultAdapter;
+ }
+ $mapper = $this->objectManager->create($verificationClass);
+ if (!$mapper instanceof PaymentVerificationInterface) {
+ throw new ConfigurationMismatchException(
+ __('%1 must implement %2', $verificationClass, PaymentVerificationInterface::class)
+ );
+ }
+ return $mapper;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/PredefinedVerificationCode.php b/app/code/Magento/Signifyd/Model/PredefinedVerificationCode.php
new file mode 100644
index 0000000000000..618d74b2a52e9
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/PredefinedVerificationCode.php
@@ -0,0 +1,37 @@
+code = $code;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCode(OrderPaymentInterface $orderPayment)
+ {
+ return $this->code;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/QuoteSession/Adminhtml/BackendSession.php b/app/code/Magento/Signifyd/Model/QuoteSession/Adminhtml/BackendSession.php
new file mode 100644
index 0000000000000..9be02719545c7
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/QuoteSession/Adminhtml/BackendSession.php
@@ -0,0 +1,40 @@
+backendQuoteSession = $backendQuoteSession;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQuote()
+ {
+ return $this->backendQuoteSession->getQuote();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/QuoteSession/FrontendSession.php b/app/code/Magento/Signifyd/Model/QuoteSession/FrontendSession.php
new file mode 100644
index 0000000000000..44c226ae4a47e
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/QuoteSession/FrontendSession.php
@@ -0,0 +1,39 @@
+checkoutSession = $checkoutSession;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQuote()
+ {
+ return $this->checkoutSession->getQuote();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/QuoteSession/QuoteSessionInterface.php b/app/code/Magento/Signifyd/Model/QuoteSession/QuoteSessionInterface.php
new file mode 100644
index 0000000000000..14958ac65a6ee
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/QuoteSession/QuoteSessionInterface.php
@@ -0,0 +1,19 @@
+_init('signifyd_case', 'entity_id');
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/ResourceModel/CaseEntity/Collection.php b/app/code/Magento/Signifyd/Model/ResourceModel/CaseEntity/Collection.php
new file mode 100644
index 0000000000000..92e233dd42dbc
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/ResourceModel/CaseEntity/Collection.php
@@ -0,0 +1,24 @@
+_init(CaseEntity::class, CaseResourceModel::class);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SalesOrderGrid/NotSyncedOrderIdListProvider.php b/app/code/Magento/Signifyd/Model/SalesOrderGrid/NotSyncedOrderIdListProvider.php
new file mode 100644
index 0000000000000..360225ae37b7b
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SalesOrderGrid/NotSyncedOrderIdListProvider.php
@@ -0,0 +1,54 @@
+caseEntity = $caseEntity;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getIds($mainTableName, $gridTableName)
+ {
+ $connection = $this->caseEntity->getConnection();
+ $select = $connection->select()
+ ->from($this->caseEntity->getMainTable(), ['order_id'])
+ ->joinLeft(
+ [$gridTableName => $connection->getTableName($gridTableName)],
+ sprintf(
+ '%s.%s = %s.%s',
+ $this->caseEntity->getMainTable(),
+ 'order_id',
+ $gridTableName,
+ 'entity_id'
+ ),
+ []
+ )
+ ->where('guarantee_disposition != signifyd_guarantee_status');
+
+ return $connection->fetchAll($select, [], \Zend_Db::FETCH_COLUMN);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SalesOrderGrid/OrderGridUpdater.php b/app/code/Magento/Signifyd/Model/SalesOrderGrid/OrderGridUpdater.php
new file mode 100644
index 0000000000000..fff42b300be58
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SalesOrderGrid/OrderGridUpdater.php
@@ -0,0 +1,55 @@
+globalConfig = $globalConfig;
+ $this->entityGrid = $entityGrid;
+ }
+
+ /**
+ * Handles synchronous updating order entity in grid.
+ *
+ * Works only if asynchronous grid indexing is disabled
+ * in global settings.
+ *
+ * @param int $orderId
+ * @return void
+ */
+ public function update($orderId)
+ {
+ if (!$this->globalConfig->getValue('dev/grid/async_indexing')) {
+ $this->entityGrid->refresh($orderId);
+ }
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiCallException.php b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiCallException.php
new file mode 100644
index 0000000000000..73338c8ea4d62
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiCallException.php
@@ -0,0 +1,39 @@
+requestData = $requestData;
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Gets request data for unsuccessful request in JSON format
+ * @return string
+ */
+ public function getRequestData()
+ {
+ return $this->requestData;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php
new file mode 100644
index 0000000000000..0950ca1e22cfa
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php
@@ -0,0 +1,48 @@
+requestBuilder = $requestBuilder;
+ }
+
+ /**
+ * Perform call to Signifyd API.
+ *
+ * Method returns associative array that corresponds to successful result.
+ * Current implementation do not expose details in case of failure.
+ *
+ * @param string $url
+ * @param string $method
+ * @param array $params
+ * @return array
+ */
+ public function makeApiCall($url, $method, array $params = [])
+ {
+ $result = $this->requestBuilder->doRequest($url, $method, $params);
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php
new file mode 100644
index 0000000000000..41006bd7d1e0e
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php
@@ -0,0 +1,142 @@
+config = $config;
+ $this->clientFactory = $clientFactory;
+ $this->dataEncoder = $dataEncoder;
+ }
+
+ /**
+ * Creates and configures HTTP client.
+ *
+ * @param string $url
+ * @param string $method
+ * @param array $params
+ * @return ZendClient
+ */
+ public function create($url, $method, array $params = [])
+ {
+ $apiKey = $this->getApiKey();
+ $apiUrl = $this->buildFullApiUrl($url);
+
+ $client = $this->createNewClient();
+ $client->setHeaders(
+ self::$authorizationType,
+ sprintf('Basic %s', base64_encode($apiKey))
+ );
+ if (!empty($params)) {
+ $encodedData = $this->dataEncoder->encode($params);
+ $client->setRawData($encodedData, self::$jsonDataType);
+ }
+ $client->setMethod($method);
+ $client->setUri($apiUrl);
+
+ return $client;
+ }
+
+ /**
+ * @return ZendClient
+ */
+ private function createNewClient()
+ {
+ return $this->clientFactory->create();
+ }
+
+ /**
+ * Signifyd API key for merchant account.
+ *
+ * @see https://www.signifyd.com/docs/api/#/introduction/authentication
+ * @return string
+ */
+ private function getApiKey()
+ {
+ return $this->config->getApiKey();
+ }
+
+ /**
+ * Full URL for Singifyd API based on relative URL.
+ *
+ * @param string $url
+ * @return string
+ */
+ private function buildFullApiUrl($url)
+ {
+ $baseApiUrl = $this->getBaseApiUrl();
+ $fullUrl = $baseApiUrl . self::$urlSeparator . ltrim($url, self::$urlSeparator);
+
+ return $fullUrl;
+ }
+
+ /**
+ * Base Sigifyd API URL without trailing slash.
+ *
+ * @return string
+ */
+ private function getBaseApiUrl()
+ {
+ $baseApiUrl = $this->config->getApiUrl();
+
+ return rtrim($baseApiUrl, self::$urlSeparator);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php
new file mode 100644
index 0000000000000..2ab4395e1990d
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php
@@ -0,0 +1,64 @@
+clientCreator = $clientCreator;
+ $this->requestSender = $requestSender;
+ $this->responseHandler = $responseHandler;
+ }
+
+ /**
+ * Creates HTTP client for API call.
+ *
+ * @param string $url
+ * @param string $method
+ * @param array $params
+ * @return array
+ */
+ public function doRequest($url, $method, array $params = [])
+ {
+ $client = $this->clientCreator->create($url, $method, $params);
+ $response = $this->requestSender->send($client);
+ $result = $this->responseHandler->handle($response);
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php
new file mode 100644
index 0000000000000..38128a799fd59
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php
@@ -0,0 +1,73 @@
+debuggerFactory = $debuggerFactory;
+ }
+
+ /**
+ * Sends HTTP request to Signifyd API with configured client.
+ *
+ * Each request/response pair is handled by debugger.
+ * If debug mode for Signifyd integration enabled in configuration
+ * debug information is recorded to debug.log.
+ *
+ * @param ZendClient $client
+ * @return \Zend_Http_Response
+ * @throws ApiCallException
+ */
+ public function send(ZendClient $client)
+ {
+ try {
+ $response = $client->request();
+
+ $this->debuggerFactory->create()->success(
+ $client->getUri(true),
+ $client->getLastRequest(),
+ $response->getStatus() . ' ' . $response->getMessage(),
+ $response->getBody()
+ );
+
+ return $response;
+ } catch (\Exception $e) {
+ $this->debuggerFactory->create()->failure(
+ $client->getUri(true),
+ $client->getLastRequest(),
+ $e
+ );
+
+ throw new ApiCallException(
+ 'Unable to process Signifyd API: ' . $e->getMessage(),
+ $e->getCode(),
+ $e,
+ $client->getLastRequest()
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/ResponseHandler.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/ResponseHandler.php
new file mode 100644
index 0000000000000..614e59b7c29a2
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/ResponseHandler.php
@@ -0,0 +1,124 @@
+ 'Bad Request - The request could not be parsed. Response: %s',
+ 401 => 'Unauthorized - user is not logged in, could not be authenticated. Response: %s',
+ 403 => 'Forbidden - Cannot access resource. Response: %s',
+ 404 => 'Not Found - resource does not exist. Response: %s',
+ 409 => 'Conflict - with state of the resource on server. Can occur with (too rapid) PUT requests. Response: %s',
+ 500 => 'Server error. Response: %s'
+ ];
+
+ /**
+ * Unexpected Signifyd API response message.
+ *
+ * @var string
+ */
+ private static $unexpectedResponse = 'Unexpected Signifyd API response code "%s" with content "%s".';
+
+ /**
+ * @var DecoderInterface
+ */
+ private $dataDecoder;
+
+ /**
+ * ResponseHandler constructor.
+ *
+ * @param DecoderInterface $dataDecoder
+ */
+ public function __construct(
+ DecoderInterface $dataDecoder
+ ) {
+ $this->dataDecoder = $dataDecoder;
+ }
+
+ /**
+ * Reads result of successful operation and throws exception in case of any failure.
+ *
+ * @param \Zend_Http_Response $response
+ * @return array
+ * @throws ApiCallException
+ */
+ public function handle(\Zend_Http_Response $response)
+ {
+ $responseCode = $response->getStatus();
+
+ if (!in_array($responseCode, self::$successResponseCodes)) {
+ $errorMessage = $this->buildApiCallFailureMessage($response);
+ throw new ApiCallException($errorMessage);
+ }
+
+ $responseBody = (string)$response->getBody();
+
+ if (self::$phpVersionId < 70000 && empty($responseBody)) {
+ /*
+ * Only since PHP 7.0 empty string treated as JSON syntax error
+ * http://php.net/manual/en/function.json-decode.php
+ */
+ throw new ApiCallException('Response is not valid JSON: Decoding failed: Syntax error');
+ }
+
+ try {
+ $decodedResponseBody = $this->dataDecoder->decode($responseBody);
+ } catch (\Exception $e) {
+ throw new ApiCallException(
+ 'Response is not valid JSON: ' . $e->getMessage(),
+ $e->getCode(),
+ $e
+ );
+ }
+
+ return $decodedResponseBody;
+ }
+
+ /**
+ * Error message for request rejected by Signify.
+ *
+ * @param \Zend_Http_Response $response
+ * @return string
+ */
+ private function buildApiCallFailureMessage(\Zend_Http_Response $response)
+ {
+ $responseBody = $response->getBody();
+
+ if (key_exists($response->getStatus(), self::$failureResponses)) {
+ return sprintf(self::$failureResponses[$response->getStatus()], $responseBody);
+ }
+
+ return sprintf(
+ self::$unexpectedResponse,
+ $response->getStatus(),
+ $responseBody
+ );
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/BlackHole.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/BlackHole.php
new file mode 100644
index 0000000000000..7057313b5e415
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/BlackHole.php
@@ -0,0 +1,33 @@
+ Configuration > Sales > Fraud Detection > Signifyd > Debug)
+ */
+class DebuggerFactory
+{
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @var Config
+ */
+ private $config;
+
+ /**
+ * DebuggerFactory constructor.
+ *
+ * @param bjectManagerInterface $objectManager
+ * @param Config $config
+ */
+ public function __construct(
+ ObjectManagerInterface $objectManager,
+ Config $config
+ ) {
+ $this->objectManager = $objectManager;
+ $this->config = $config;
+ }
+
+ /**
+ * Create debugger instance
+ *
+ * @return DebuggerInterface
+ */
+ public function create()
+ {
+ if (!$this->config->isDebugModeEnabled()) {
+ return $this->objectManager->get(BlackHole::class);
+ }
+
+ return $this->objectManager->get(Log::class);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerInterface.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerInterface.php
new file mode 100644
index 0000000000000..f4a2f9cc56a8f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerInterface.php
@@ -0,0 +1,35 @@
+logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function success($requestUrl, $requestData, $responseStatus, $responseBody)
+ {
+ $requestInfo = $this->buildRequestInfo($requestUrl, $requestData);
+ $responseInfo = $this->buildResponseInfo($responseStatus, $responseBody);
+
+ $info = $requestInfo
+ . $responseInfo;
+
+ $this->writeToLog($info);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function failure($requestUrl, $requestData, Exception $exception)
+ {
+ $requestInfo = $this->buildRequestInfo($requestUrl, $requestData);
+ $exceptionInfo = $this->buildExceptionInfo($exception);
+
+ $info = $requestInfo
+ . $exceptionInfo;
+
+ $this->writeToLog($info);
+ }
+
+ /**
+ * Build string with request URL and body
+ *
+ * @param string $requestUrl
+ * @param string $requestData
+ * @return string
+ */
+ private function buildRequestInfo($requestUrl, $requestData)
+ {
+ $infoContent = $this->buildInfoSection('URL', $requestUrl)
+ . $this->buildInfoSection('Body', $requestData);
+
+ $info = $this->buildInfoSection('Request', $infoContent);
+ return $info;
+ }
+
+ /**
+ * Build string with response status code and body
+ *
+ * @param string $responseStatus
+ * @param string $responseBody
+ * @return string
+ */
+ private function buildResponseInfo($responseStatus, $responseBody)
+ {
+ $infoContent = $this->buildInfoSection('Status', $responseStatus)
+ . $this->buildInfoSection('Body', $responseBody);
+
+ $info = $this->buildInfoSection('Response', $infoContent);
+ return $info;
+ }
+
+ /**
+ * Build string with exception information
+ *
+ * @param Exception $exception
+ * @return string
+ */
+ private function buildExceptionInfo(Exception $exception)
+ {
+ $infoContent = (string)$exception;
+ $info = $this->buildInfoSection('Exception', $infoContent);
+ return $info;
+ }
+
+ /**
+ * Write debug information to log file (var/log/debug.log by default)
+ *
+ * @param string $info
+ * @return void
+ */
+ private function writeToLog($info)
+ {
+ $logMessage = $this->buildInfoSection('Signifyd API integration debug info', $info);
+ $this->logger->debug($logMessage);
+ }
+
+ /**
+ * Build unified debug section string
+ *
+ * @param string $title
+ * @param string $content
+ * @return string
+ */
+ private function buildInfoSection($title, $content)
+ {
+ $formattedInfo = $title . ":\n"
+ . $this->addIndent($content) . "\n";
+ return $formattedInfo;
+ }
+
+ /**
+ * Add indent to each line in content
+ *
+ * @param string $content
+ * @param string $indent
+ * @return string
+ */
+ private function addIndent($content, $indent = ' ')
+ {
+ $contentLines = explode("\n", $content);
+ $contentLinesWithIndent = array_map(function ($line) use ($indent) {
+ return $indent . $line;
+ }, $contentLines);
+ $content = implode("\n", $contentLinesWithIndent);
+ return $content;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php
new file mode 100644
index 0000000000000..ddcaa6cd696f2
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php
@@ -0,0 +1,175 @@
+createCaseBuilder = $createCaseBuilder;
+ $this->apiClient = $apiClient;
+ }
+
+ /**
+ * Returns id of created case (investigation) on Signifyd service
+ * @see https://www.signifyd.com/docs/api/#/reference/cases/create-a-case
+ *
+ * @param int $orderId
+ * @return int Signifyd case (investigation) identifier
+ * @throws GatewayException
+ */
+ public function createCase($orderId)
+ {
+ $caseParams = $this->createCaseBuilder->build($orderId);
+
+ $caseCreationResult = $this->apiClient->makeApiCall(
+ '/cases',
+ 'POST',
+ $caseParams
+ );
+
+ if (!isset($caseCreationResult['investigationId'])) {
+ throw new GatewayException('Expected field "investigationId" missed.');
+ }
+
+ return (int)$caseCreationResult['investigationId'];
+ }
+
+ /**
+ * Returns guaranty decision result
+ * @see https://www.signifyd.com/docs/api/#/reference/guarantees/submit-a-case-for-guarantee
+ *
+ * @param int $signifydCaseId
+ * @return string
+ * @throws GatewayException
+ */
+ public function submitCaseForGuarantee($signifydCaseId)
+ {
+ $guaranteeCreationResult = $this->apiClient->makeApiCall(
+ '/guarantees',
+ 'POST',
+ [
+ 'caseId' => $signifydCaseId,
+ ]
+ );
+
+ $disposition = $this->processDispositionResult($guaranteeCreationResult);
+ return $disposition;
+ }
+
+ /**
+ * Sends request to cancel guarantee and returns disposition.
+ *
+ * @see https://www.signifyd.com/docs/api/#/reference/guarantees/submit-a-case-for-guarantee/cancel-guarantee
+ * @param int $caseId
+ * @return string
+ * @throws GatewayException
+ */
+ public function cancelGuarantee($caseId)
+ {
+ $result = $this->apiClient->makeApiCall(
+ '/cases/' . $caseId . '/guarantee',
+ 'PUT',
+ [
+ 'guaranteeDisposition' => self::GUARANTEE_CANCELED
+ ]
+ );
+
+ $disposition = $this->processDispositionResult($result);
+ if ($disposition !== self::GUARANTEE_CANCELED) {
+ throw new GatewayException("API returned unexpected disposition: $disposition.");
+ }
+
+ return $disposition;
+ }
+
+ /**
+ * Processes result from Signifyd API.
+ * Throws the GatewayException is result does not contain guarantee disposition in response or
+ * disposition has unknown status.
+ *
+ * @param array $result
+ * @return string
+ * @throws GatewayException
+ */
+ private function processDispositionResult(array $result)
+ {
+ if (!isset($result['disposition'])) {
+ throw new GatewayException('Expected field "disposition" missed.');
+ }
+
+ $disposition = strtoupper($result['disposition']);
+
+ if (!in_array($disposition, [
+ self::GUARANTEE_APPROVED,
+ self::GUARANTEE_DECLINED,
+ self::GUARANTEE_PENDING,
+ self::GUARANTEE_CANCELED,
+ self::GUARANTEE_IN_REVIEW,
+ self::GUARANTEE_UNREQUESTED
+ ])) {
+ throw new GatewayException(
+ sprintf('API returns unknown guaranty disposition "%s".', $disposition)
+ );
+ }
+
+ return $disposition;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/GatewayException.php b/app/code/Magento/Signifyd/Model/SignifydGateway/GatewayException.php
new file mode 100644
index 0000000000000..666217f8ccc85
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/GatewayException.php
@@ -0,0 +1,14 @@
+ $this->getStreetLine(1, $address->getStreet()),
+ 'unit' => $this->getStreetLine(2, $address->getStreet()),
+ 'city' => $address->getCity(),
+ 'provinceCode' => $address->getRegionCode(),
+ 'postalCode' => $address->getPostcode(),
+ 'countryCode' => $address->getCountryId()
+ ];
+ }
+
+ /**
+ * Get street line by number
+ *
+ * @param int $number
+ * @param string[]|null $street
+ * @return string
+ */
+ private function getStreetLine($number, $street)
+ {
+ $lines = is_array($street) ? $street : [];
+
+ return isset($lines[$number - 1]) ? $lines[$number - 1] : '';
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CardBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CardBuilder.php
new file mode 100644
index 0000000000000..5e3a1a83e7aeb
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CardBuilder.php
@@ -0,0 +1,56 @@
+addressBuilder = $addressBuilder;
+ }
+
+ /**
+ * Returns card data params based on payment and billing address info
+ *
+ * @param Order $order
+ * @return array
+ */
+ public function build(Order $order)
+ {
+ $result = [];
+ $address = $order->getBillingAddress();
+ if ($address === null) {
+ return $result;
+ }
+
+ $payment = $order->getPayment();
+ $result = [
+ 'card' => [
+ 'cardHolderName' => $address->getFirstname() . ' ' . $address->getLastname(),
+ 'last4' => $payment->getCcLast4(),
+ 'expiryMonth' => $payment->getCcExpMonth(),
+ 'expiryYear' => $payment->getCcExpYear(),
+ 'billingAddress' => $this->addressBuilder->build($address)
+ ]
+ ];
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/ClientVersionBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/ClientVersionBuilder.php
new file mode 100644
index 0000000000000..8db06473b96d8
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/ClientVersionBuilder.php
@@ -0,0 +1,50 @@
+productMetadata = $productMetadata;
+ }
+
+ /**
+ * Returns version info
+ *
+ * @return array
+ */
+ public function build()
+ {
+ return [
+ 'platformAndClient' => [
+ 'storePlatform' => $this->productMetadata->getName() . ' ' . $this->productMetadata->getEdition(),
+ 'storePlatformVersion' => $this->productMetadata->getVersion(),
+ 'signifydClientApp' => $this->productMetadata->getName(),
+ 'signifydClientAppVersion' => self::$clientVersion,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilder.php
new file mode 100644
index 0000000000000..3e41003d47842
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilder.php
@@ -0,0 +1,131 @@
+orderFactory = $orderFactory;
+ $this->purchaseBuilder = $purchaseBuilder;
+ $this->cardBuilder = $cardBuilder;
+ $this->recipientBuilder = $recipientBuilder;
+ $this->sellerBuilder = $sellerBuilder;
+ $this->clientVersionBuilder = $clientVersionBuilder;
+ $this->userAccountBuilder = $userAccountBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function build($orderId)
+ {
+ /* @var $order \Magento\Sales\Model\Order */
+ $order = $this->orderFactory->create()->load($orderId);
+
+ return $this->removeEmptyValues(
+ array_merge(
+ $this->purchaseBuilder->build($order),
+ $this->cardBuilder->build($order),
+ $this->recipientBuilder->build($order),
+ $this->userAccountBuilder->build($order),
+ $this->sellerBuilder->build($order),
+ $this->clientVersionBuilder->build()
+ )
+ );
+ }
+
+ /**
+ * Remove empty and null values.
+ *
+ * @param array $data
+ * @return array
+ */
+ private function removeEmptyValues($data)
+ {
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $data[$key] = $this->removeEmptyValues($data[$key]);
+ }
+
+ if ($this->isEmpty($data[$key])) {
+ unset($data[$key]);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Empty values are null, empty string and empty array.
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ private function isEmpty($value)
+ {
+ return $value === null || (is_array($value) && empty($value));
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderInterface.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderInterface.php
new file mode 100644
index 0000000000000..56662f69e7c5a
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderInterface.php
@@ -0,0 +1,22 @@
+dateTimeFactory = $dateTimeFactory;
+ $this->scope = $scope;
+ $this->signifydOrderSessionId = $signifydOrderSessionId;
+ $this->paymentVerificationFactory = $paymentVerificationFactory;
+ $this->paymentMethodMapper = $paymentMethodMapper;
+ }
+
+ /**
+ * Returns purchase data params
+ *
+ * @param Order $order
+ * @return array
+ */
+ public function build(Order $order)
+ {
+ $orderPayment = $order->getPayment();
+ $createdAt = $this->dateTimeFactory->create(
+ $order->getCreatedAt(),
+ new \DateTimeZone('UTC')
+ );
+
+ $result = [
+ 'purchase' => [
+ 'orderSessionId' => $this->signifydOrderSessionId->get($order->getQuoteId()),
+ 'browserIpAddress' => $order->getRemoteIp(),
+ 'orderId' => $order->getIncrementId(),
+ 'createdAt' => $createdAt->format(\DateTime::ATOM),
+ 'paymentGateway' => $this->getPaymentGateway($orderPayment->getMethod()),
+ 'transactionId' => $orderPayment->getLastTransId(),
+ 'currency' => $order->getOrderCurrencyCode(),
+ 'avsResponseCode' => $this->getAvsCode($orderPayment),
+ 'cvvResponseCode' => $this->getCvvCode($orderPayment),
+ 'orderChannel' => $this->getOrderChannel(),
+ 'totalPrice' => $order->getGrandTotal(),
+ 'paymentMethod' => $this->paymentMethodMapper
+ ->getSignifydPaymentMethodCode($orderPayment->getMethod())
+ ],
+ ];
+
+ $shippingDescription = $order->getShippingDescription();
+ if ($shippingDescription !== null) {
+ $result['purchase']['shipments'] = [
+ [
+ 'shipper' => $this->getShipper($order->getShippingDescription()),
+ 'shippingMethod' => $this->getShippingMethod($order->getShippingDescription()),
+ 'shippingPrice' => $order->getShippingAmount()
+ ]
+ ];
+ }
+
+ $products = $this->getProducts($order);
+ if (!empty($products)) {
+ $result['purchase']['products'] = $products;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the products purchased in the transaction.
+ *
+ * @param Order $order
+ * @return array
+ */
+ private function getProducts(Order $order)
+ {
+ $result = [];
+ foreach ($order->getAllItems() as $orderItem) {
+ $result[] = [
+ 'itemId' => $orderItem->getSku(),
+ 'itemName' => $orderItem->getName(),
+ 'itemPrice' => $orderItem->getPrice(),
+ 'itemQuantity' => (int)$orderItem->getQtyOrdered(),
+ 'itemUrl' => $orderItem->getProduct()->getProductUrl(),
+ 'itemWeight' => $orderItem->getProduct()->getWeight()
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the name of the shipper
+ *
+ * @param string $shippingDescription
+ * @return string
+ */
+ private function getShipper($shippingDescription)
+ {
+ $result = explode(' - ', $shippingDescription, 2);
+
+ return count($result) == 2 ? $result[0] : '';
+ }
+
+ /**
+ * Returns the type of the shipment method used
+ *
+ * @param string $shippingDescription
+ * @return string
+ */
+ private function getShippingMethod($shippingDescription)
+ {
+ $result = explode(' - ', $shippingDescription, 2);
+
+ return count($result) == 2 ? $result[1] : '';
+ }
+
+ /**
+ * Returns the gateway that processed the transaction. For PayPal orders should be paypal_account.
+ *
+ * @param string $gatewayCode
+ * @return string
+ */
+ private function getPaymentGateway($gatewayCode)
+ {
+ $payPalCodeList = [
+ 'paypal_express',
+ 'braintree_paypal',
+ 'payflowpro',
+ 'payflow_express',
+ 'payflow_link',
+ 'payflow_advanced',
+ 'hosted_pro',
+ ];
+ return in_array($gatewayCode, $payPalCodeList) ? 'paypal_account' : $gatewayCode;
+ }
+
+ /**
+ * Returns WEB for web-orders, PHONE for orders created by Admin
+ *
+ * @return string
+ */
+ private function getOrderChannel()
+ {
+ return $this->scope->getCurrentScope() === Area::AREA_ADMINHTML ? 'PHONE' : 'WEB';
+ }
+
+ /**
+ * Gets AVS code for order payment method.
+ *
+ * @param OrderPaymentInterface $orderPayment
+ * @return string
+ */
+ private function getAvsCode(OrderPaymentInterface $orderPayment)
+ {
+ $avsAdapter = $this->paymentVerificationFactory->createPaymentAvs($orderPayment->getMethod());
+ return $avsAdapter->getCode($orderPayment);
+ }
+
+ /**
+ * Gets CVV code for order payment method.
+ *
+ * @param OrderPaymentInterface $orderPayment
+ * @return string
+ */
+ private function getCvvCode(OrderPaymentInterface $orderPayment)
+ {
+ $cvvAdapter = $this->paymentVerificationFactory->createPaymentCvv($orderPayment->getMethod());
+ return $cvvAdapter->getCode($orderPayment);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/RecipientBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/RecipientBuilder.php
new file mode 100644
index 0000000000000..d9d26c8943b88
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/RecipientBuilder.php
@@ -0,0 +1,55 @@
+addressBuilder = $addressBuilder;
+ }
+
+ /**
+ * Returns recipient data params based on shipping address
+ *
+ * @param Order $order
+ * @return array
+ */
+ public function build(Order $order)
+ {
+ $result = [];
+ $address = $order->getShippingAddress();
+ if ($address === null) {
+ return $result;
+ }
+
+ $result = [
+ 'recipient' => [
+ 'fullName' => $address->getName(),
+ 'confirmationEmail' => $address->getEmail(),
+ 'confirmationPhone' => $address->getTelephone(),
+ 'organization' => $address->getCompany(),
+ 'deliveryAddress' => $this->addressBuilder->build($address)
+ ]
+ ];
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/SellerBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/SellerBuilder.php
new file mode 100644
index 0000000000000..b2cf0401b247f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/SellerBuilder.php
@@ -0,0 +1,136 @@
+scopeConfig = $scopeConfig;
+ $this->regionFactory = $regionFactory;
+ }
+
+ /**
+ * Returns seller data params
+ *
+ * @param Order $order
+ * @return array
+ */
+ public function build(Order $order)
+ {
+ $store = $order->getStore();
+
+ return [
+ 'seller' => [
+ 'name' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_NAME, $store),
+ 'domain' => $this->getPublicDomain($store),
+ 'shipFromAddress' => [
+ 'streetAddress' => $this->getConfigValue(Shipment::XML_PATH_STORE_ADDRESS1, $store),
+ 'unit' => $this->getConfigValue(Shipment::XML_PATH_STORE_ADDRESS2, $store),
+ 'city' => $this->getConfigValue(Shipment::XML_PATH_STORE_CITY, $store),
+ 'provinceCode' => $this->getRegionCodeById(
+ $this->getConfigValue(Shipment::XML_PATH_STORE_REGION_ID, $store)
+ ),
+ 'postalCode' => $this->getConfigValue(Shipment::XML_PATH_STORE_ZIP, $store),
+ 'countryCode' => $this->getConfigValue(Shipment::XML_PATH_STORE_COUNTRY_ID, $store),
+ ],
+ 'corporateAddress' => [
+ 'streetAddress' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_STREET_LINE1, $store),
+ 'unit' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_STREET_LINE2, $store),
+ 'city' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_CITY, $store),
+ 'provinceCode' => $this->getRegionCodeById(
+ $this->getConfigValue(Information::XML_PATH_STORE_INFO_REGION_CODE, $store)
+ ),
+ 'postalCode' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_POSTCODE, $store),
+ 'countryCode' => $this->getConfigValue(Information::XML_PATH_STORE_INFO_COUNTRY_CODE, $store),
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Returns region code by id
+ *
+ * @param int $regionId
+ * @return string
+ */
+ private function getRegionCodeById($regionId)
+ {
+ if (!isset($this->regionCodes[$regionId])) {
+ $this->regionCodes[$regionId] = $this->regionFactory->create()->load($regionId)->getCode();
+ }
+
+ return $this->regionCodes[$regionId];
+ }
+
+ /**
+ * Returns value from config
+ *
+ * @param string $value
+ * @param StoreInterface $store
+ * @return mixed
+ */
+ private function getConfigValue($value, StoreInterface $store)
+ {
+ return $this->scopeConfig->getValue(
+ $value,
+ ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ }
+
+ /**
+ * Returns public domain name
+ *
+ * @param StoreInterface $store
+ * @return string|null null if no DNS records corresponding to a current host found
+ */
+ private function getPublicDomain(StoreInterface $store)
+ {
+ $baseUrl = $store->getBaseUrl();
+ $domain = parse_url($baseUrl, PHP_URL_HOST);
+ if (\function_exists('checkdnsrr') && false === \checkdnsrr($domain)) {
+ return null;
+ }
+
+ return $domain;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/UserAccountBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/UserAccountBuilder.php
new file mode 100644
index 0000000000000..0da49b85da869
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/UserAccountBuilder.php
@@ -0,0 +1,100 @@
+customerRepository = $customerRepository;
+ $this->dateTimeFactory = $dateTimeFactory;
+ $this->customerOrders = $customerOrders;
+ }
+
+ /**
+ * Returns user account data params.
+ * Only for registered customers.
+ *
+ * @param Order $order
+ * @return array
+ */
+ public function build(Order $order)
+ {
+ $result = [];
+
+ $customerId = $order->getCustomerId();
+ if (null === $customerId) {
+ return $result;
+ }
+
+ $customer = $this->customerRepository->getById($customerId);
+ $result = [
+ 'userAccount' => [
+ 'email' => $customer->getEmail(),
+ 'username' => $customer->getEmail(),
+ 'phone' => $order->getBillingAddress()->getTelephone(),
+ 'accountNumber' => $customerId,
+ 'createdDate' => $this->formatDate($customer->getCreatedAt()),
+ 'lastUpdateDate' => $this->formatDate($customer->getUpdatedAt())
+ ]
+ ];
+
+ $ordersInfo = $this->customerOrders->getAggregatedOrdersInfo($customerId);
+ if (isset($ordersInfo['aggregateOrderCount'])) {
+ $result['userAccount']['aggregateOrderCount'] = $ordersInfo['aggregateOrderCount'];
+ }
+ if (isset($ordersInfo['aggregateOrderDollars'])) {
+ $result['userAccount']['aggregateOrderDollars'] = $ordersInfo['aggregateOrderDollars'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns date formatted according to ISO8601.
+ *
+ * @param string $date
+ * @return string
+ */
+ private function formatDate($date)
+ {
+ $result = $this->dateTimeFactory->create(
+ $date,
+ new \DateTimeZone('UTC')
+ );
+
+ return $result->format(\DateTime::ATOM);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessage.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessage.php
new file mode 100644
index 0000000000000..f77db737473c0
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessage.php
@@ -0,0 +1,64 @@
+data = $data;
+ $this->eventTopic = $eventTopic;
+ }
+
+ /**
+ * Returns decoded webhook request body.
+ *
+ * @return array
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Returns event topic identifier.
+ *
+ * @return string
+ */
+ public function getEventTopic()
+ {
+ return $this->eventTopic;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessageReader.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessageReader.php
new file mode 100644
index 0000000000000..50389102359b1
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookMessageReader.php
@@ -0,0 +1,65 @@
+dataDecoder = $decoder;
+ $this->webhookMessageFactory = $webhookMessageFactory;
+ }
+
+ /**
+ * Returns webhook message data object.
+ *
+ * @param WebhookRequest $request
+ * @return WebhookMessage
+ * @throws \InvalidArgumentException
+ */
+ public function read(WebhookRequest $request)
+ {
+ try {
+ $decodedData = $this->dataDecoder->decode($request->getBody());
+ } catch (\Exception $e) {
+ throw new \InvalidArgumentException(
+ 'Webhook request body is not valid JSON: ' . $e->getMessage(),
+ $e->getCode(),
+ $e
+ );
+ }
+
+ $webhookMessage = $this->webhookMessageFactory->create(
+ [
+ 'data' => $decodedData,
+ 'eventTopic' => $request->getEventTopic()
+ ]
+ );
+
+ return $webhookMessage;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequest.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequest.php
new file mode 100644
index 0000000000000..214ccf0eeb70f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequest.php
@@ -0,0 +1,58 @@
+request = $request;
+ }
+
+ /**
+ * Returns Base64 encoded output of the HMAC SHA256 encoding of the JSON body of the message.
+ *
+ * @return string
+ */
+ public function getHash()
+ {
+ return (string)$this->request->getHeader('X-SIGNIFYD-SEC-HMAC-SHA256');
+ }
+
+ /**
+ * Returns event topic identifier.
+ *
+ * @return string
+ */
+ public function getEventTopic()
+ {
+ return (string)$this->request->getHeader('X-SIGNIFYD-TOPIC');
+ }
+
+ /**
+ * Returns raw data from the request body.
+ *
+ * @return string
+ */
+ public function getBody()
+ {
+ return (string)$this->request->getContent();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequestValidator.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequestValidator.php
new file mode 100644
index 0000000000000..274ef2f854684
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Response/WebhookRequestValidator.php
@@ -0,0 +1,113 @@
+config = $config;
+ $this->decoder = $decoder;
+ }
+
+ /**
+ * Validates webhook request.
+ *
+ * @param WebhookRequest $webhookRequest
+ * @return bool
+ */
+ public function validate(WebhookRequest $webhookRequest)
+ {
+ $body = $webhookRequest->getBody();
+ $eventTopic = $webhookRequest->getEventTopic();
+ $hash = $webhookRequest->getHash();
+
+ return $this->isValidTopic($eventTopic)
+ && $this->isValidBody($body)
+ && $this->isValidHash($eventTopic, $body, $hash);
+ }
+
+ /**
+ * Checks if value of topic identifier is in allowed list
+ *
+ * @param string $topic topic identifier.
+ * @return bool
+ */
+ private function isValidTopic($topic)
+ {
+ return in_array($topic, $this->allowedTopicValues);
+ }
+
+ /**
+ * Verifies a webhook request body is valid JSON and not empty.
+ *
+ * @param string $body
+ * @return bool
+ */
+ private function isValidBody($body)
+ {
+ try {
+ $decodedBody = $this->decoder->decode($body);
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return !empty($decodedBody);
+ }
+
+ /**
+ * Verifies a webhook request has in fact come from SIGNIFYD.
+ *
+ * @param string $eventTopic
+ * @param string $body
+ * @param string $hash
+ * @return bool
+ */
+ private function isValidHash($eventTopic, $body, $hash)
+ {
+ // In the case that this is a webhook test, the encoding ABCDE is allowed
+ $apiKey = $eventTopic == 'cases/test' ? 'ABCDE' : $this->config->getApiKey();
+ $actualHash = base64_encode(hash_hmac('sha256', $body, $apiKey, true));
+
+ return $hash === $actualHash;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Model/SignifydOrderSessionId.php b/app/code/Magento/Signifyd/Model/SignifydOrderSessionId.php
new file mode 100644
index 0000000000000..52746bc4ec6c5
--- /dev/null
+++ b/app/code/Magento/Signifyd/Model/SignifydOrderSessionId.php
@@ -0,0 +1,39 @@
+identityGenerator = $identityGenerator;
+ }
+
+ /**
+ * Returns unique identifier through generation uuid by quote id.
+ *
+ * @param int $quoteId
+ * @return string
+ */
+ public function get($quoteId)
+ {
+ return $this->identityGenerator->generateIdForData($quoteId);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Observer/PlaceOrder.php b/app/code/Magento/Signifyd/Observer/PlaceOrder.php
new file mode 100644
index 0000000000000..3798522dbe506
--- /dev/null
+++ b/app/code/Magento/Signifyd/Observer/PlaceOrder.php
@@ -0,0 +1,110 @@
+signifydIntegrationConfig = $signifydIntegrationConfig;
+ $this->caseCreationService = $caseCreationService;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute(Observer $observer)
+ {
+ if (!$this->signifydIntegrationConfig->isActive()) {
+ return;
+ }
+
+ $orders = $this->extractOrders(
+ $observer->getEvent()
+ );
+
+ if (null === $orders) {
+ return;
+ }
+
+ foreach ($orders as $order) {
+ $this->createCaseForOrder($order);
+ }
+ }
+
+ /**
+ * Creates Signifyd case for single order with online payment method.
+ *
+ * @param OrderInterface $order
+ * @return void
+ */
+ private function createCaseForOrder($order)
+ {
+ $orderId = $order->getEntityId();
+ if (null === $orderId || $order->getPayment()->getMethodInstance()->isOffline()) {
+ return;
+ }
+
+ try {
+ $this->caseCreationService->createForOrder($orderId);
+ } catch (AlreadyExistsException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Returns Orders entity list from Event data container
+ *
+ * @param Event $event
+ * @return OrderInterface[]|null
+ */
+ private function extractOrders(Event $event)
+ {
+ $order = $event->getData('order');
+ if (null !== $order) {
+ return [$order];
+ }
+
+ return $event->getData('orders');
+ }
+}
diff --git a/app/code/Magento/Signifyd/Plugin/OrderPlugin.php b/app/code/Magento/Signifyd/Plugin/OrderPlugin.php
new file mode 100644
index 0000000000000..663409d0eb824
--- /dev/null
+++ b/app/code/Magento/Signifyd/Plugin/OrderPlugin.php
@@ -0,0 +1,52 @@
+guaranteeCancelingService = $guaranteeCancelingService;
+ }
+
+ /**
+ * Performs Signifyd guarantee cancel operation after order canceling
+ * if cancel order operation was successful.
+ *
+ * @see Order::cancel
+ * @param Order $order
+ * @param OrderInterface $result
+ * @return OrderInterface
+ */
+ public function afterCancel(Order $order, $result)
+ {
+ if ($order->isCanceled()) {
+ $this->guaranteeCancelingService->cancelForOrder(
+ $order->getEntityId()
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Plugin/PaymentPlugin.php b/app/code/Magento/Signifyd/Plugin/PaymentPlugin.php
new file mode 100644
index 0000000000000..17cf4d7e7dbe9
--- /dev/null
+++ b/app/code/Magento/Signifyd/Plugin/PaymentPlugin.php
@@ -0,0 +1,67 @@
+guaranteeCancelingService = $guaranteeCancelingService;
+ }
+
+ /**
+ * Performs Signifyd guarantee cancel operation after payment denying.
+ *
+ * @see MethodInterface::denyPayment
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param MethodInterface $subject
+ * @param MethodInterface|bool $result
+ * @param InfoInterface $payment
+ * @return bool|MethodInterface
+ */
+ public function afterDenyPayment(MethodInterface $subject, $result, InfoInterface $payment)
+ {
+ if ($this->isPaymentDenied($payment, $result)) {
+ $this->guaranteeCancelingService->cancelForOrder($payment->getParentId());
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks if deny payment operation was successful.
+ *
+ * Result not false check for payment methods using AbstractMethod.
+ * Transaction is closed check for payment methods using Gateway.
+ *
+ * @param InfoInterface $payment
+ * @param MethodInterface $result
+ * @return bool
+ */
+ private function isPaymentDenied($payment, $result)
+ {
+ return $result !== false || $payment->getIsTransactionClosed();
+ }
+}
diff --git a/app/code/Magento/Signifyd/README.md b/app/code/Magento/Signifyd/README.md
new file mode 100644
index 0000000000000..9479972cb21b6
--- /dev/null
+++ b/app/code/Magento/Signifyd/README.md
@@ -0,0 +1,78 @@
+# Magento_Signifyd module
+
+## Overview
+
+The Magento_Signifyd module provides integration with the [Signifyd](https://www.signifyd.com/) fraud protection system. The integration is based on the Signifyd API; see the [Signifyd API docs](https://www.signifyd.com/docs/api/#/introduction/) for technical details.
+
+The module implementation allows to:
+
+ - create a [Signifyd case](https://www.signifyd.com/docs/api/#/reference/cases) for a placed order
+ - automatically receive a [Signifyd guarantee](https://www.signifyd.com/docs/api/#/reference/guarantees) for a created case
+ - automatically cancel a guarantee when the order is canceled
+
+## Extensibility
+
+The Magento_Signifyd module does not add own Events, Layouts, and UI Components as extension points.
+
+### Public API
+
+The following interfaces (marked with the `@api` annotation) provide methods that allow to:
+
+`Magento\Signifyd\Api\Data\CaseInterface` (common abstraction for the Signifyd case entity):
+
+- set or retrieve all case data fields
+
+`Magento\Signifyd\Api\CaseManagementInterface`:
+
+- create a new case entity
+- retrieve the existing case entity for a specified order
+
+`Magento\Signifyd\Api\CaseCreationServiceInterface`:
+
+- create a case entity for a specified order
+- send a request through the Signifyd API to create a new case
+
+`Magento\Signifyd\Api\CaseRepositoryInterface`:
+
+- describe methods to work with a case entity
+
+`Magento\Signifyd\Api\GuaranteeCreationServiceInterface`:
+
+- send a request through the Signifyd API to create a new case guarantee
+
+`Magento\Signifyd\Api\GuaranteeCancelingServiceInterface`:
+- send a request through the Signifyd API to cancel the Signifyd case guarantee
+
+`Magento\Signifyd\Api\Data\CaseSearchResultsInterface`:
+
+- might be used by `Magento\Signifyd\Api\CaseRepositoryInterface` to retrieve a list of case entities by specific conditions
+
+For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.1/extension-dev-guide/api-concepts.html).
+
+## Additional information
+
+### Webhooks
+
+To update the entity data for a case or guarantee, the Magento_Signifyd module uses the [Signifyd Webhooks](https://www.signifyd.com/docs/api/#/reference/webhooks) mechanism.
+
+The newly created case entities have the `PENDING` status for a case and a guarantee. After receiving Webhook, both statuses are changed to appropriate Signifyd statuses.
+
+### Debug mode
+
+The Debug Mode may be enabled in the module configuration. This logs the communication data between the Magento_Signifyd module and the Signifyd service in this file:
+
+ var/log/debug.log
+
+### Backward incompatible changes
+
+The Magento_Signifyd module does not introduce backward incompatible changes.
+
+You can track [backward incompatible changes in patch releases](http://devdocs.magento.com/guides/v2.0/release-notes/changes/ee_changes.html).
+
+### Processing supplementary payment information
+
+To improve the accuracy of Signifyd's transaction estimation, you may perform these operations (links lead to the Magento Developer Documentation Portal):
+
+- [Provide custom AVS/CVV mapping](http://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#provide-avscvv-response-codes)
+
+- [Retrieve payment method for a placed order](http://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#retrieve-payment-method-for-a-placed-order)
diff --git a/app/code/Magento/Signifyd/Test/Unit/Block/Adminhtml/CaseInfoTest.php b/app/code/Magento/Signifyd/Test/Unit/Block/Adminhtml/CaseInfoTest.php
new file mode 100644
index 0000000000000..164cd8018fb69
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Block/Adminhtml/CaseInfoTest.php
@@ -0,0 +1,150 @@
+context = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->request = $this->getMockBuilder(RequestInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->context->expects(self::once())
+ ->method('getRequest')
+ ->willReturn($this->request);
+
+ $this->config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->caseManagement = $this->getMockBuilder(CaseManagement::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getScore'])
+ ->getMockForAbstractClass();
+
+ $this->caseInfo = $objectManager->getObject(CaseInfo::class, [
+ 'context' => $this->context,
+ 'config' => $this->config,
+ 'caseManagement' => $this->caseManagement
+ ]);
+ }
+
+ /**
+ * Checks label according to Signifyd Guarantee Disposition.
+ *
+ * @param string $guaranteeDisposition
+ * @param string $expectedLabel
+ * @covers \Magento\Signifyd\Block\Adminhtml\CaseInfo::getCaseGuaranteeDisposition()
+ * @dataProvider getGuaranteeLabelDataProvider
+ */
+ public function testGetGuaranteeDisposition($guaranteeDisposition, $expectedLabel)
+ {
+ $this->caseManagement->expects(self::once())
+ ->method('getByOrderId')
+ ->willReturn($this->caseEntity);
+
+ $this->caseEntity->expects(self::atLeastOnce())
+ ->method('getGuaranteeDisposition')
+ ->willReturn($guaranteeDisposition);
+
+ self::assertEquals(
+ $expectedLabel,
+ $this->caseInfo->getCaseGuaranteeDisposition()
+ );
+ }
+
+ /**
+ * Case Guarantee Disposition and corresponding label data provider.
+ *
+ * @return array
+ */
+ public function getGuaranteeLabelDataProvider()
+ {
+ return [
+ [CaseInterface::GUARANTEE_APPROVED, __('Approved')],
+ [CaseInterface::GUARANTEE_DECLINED, __('Declined')],
+ [CaseInterface::GUARANTEE_PENDING, __('Pending')],
+ [CaseInterface::GUARANTEE_CANCELED, __('Canceled')],
+ [CaseInterface::GUARANTEE_IN_REVIEW, __('In Review')],
+ [CaseInterface::GUARANTEE_UNREQUESTED, __('Unrequested')],
+ ['Unregistered', '']
+ ];
+ }
+
+ /**
+ * Checks case property getter with empty case.
+ *
+ * @covers \Magento\Signifyd\Block\Adminhtml\CaseInfo::getCaseProperty
+ */
+ public function testCasePropertyWithEmptyCase()
+ {
+ $this->caseManagement->expects(self::once())
+ ->method('getByOrderId')
+ ->willReturn(null);
+
+ self::assertEquals(
+ '',
+ $this->caseInfo->getCaseGuaranteeDisposition()
+ );
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php b/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php
new file mode 100644
index 0000000000000..1a8cfdc703247
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php
@@ -0,0 +1,374 @@
+context = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->webhookRequest = $this->getMockBuilder(WebhookRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->webhookMessageReader = $this->getMockBuilder(WebhookMessageReader::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->webhookRequestValidator = $this->getMockBuilder(WebhookRequestValidator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->caseUpdatingServiceFactory = $this->getMockBuilder(UpdatingServiceFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->response = $this->getMockBuilder(ResponseHttp::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->context->expects($this->once())
+ ->method('getResponse')
+ ->willReturn($this->response);
+ $this->redirect = $this->getMockBuilder(RedirectInterface::class)
+ ->getMockForAbstractClass();
+ $this->context->expects($this->once())
+ ->method('getRedirect')
+ ->willReturn($this->redirect);
+ $this->caseRepository = $this->getMockBuilder(CaseRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getByCaseId'])
+ ->getMockForAbstractClass();
+
+ $config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isDebugModeEnabled', 'getByCaseId'])
+ ->getMock();
+ $config->expects(self::any())
+ ->method('getByCaseId')
+ ->willReturn(false);
+
+ $this->controller = new Handler(
+ $this->context,
+ $this->webhookRequest,
+ $this->logger,
+ $this->webhookMessageReader,
+ $this->caseUpdatingServiceFactory,
+ $this->webhookRequestValidator,
+ $this->caseRepository,
+ $config
+ );
+ }
+
+ /**
+ * Successfull case
+ */
+ public function testExecuteSuccessfully()
+ {
+ $eventTopic = 'cases/creation';
+ $caseId = 1;
+ $data = ['score' => 200, 'caseId' => $caseId];
+
+ $this->webhookRequestValidator->expects($this->once())
+ ->method('validate')
+ ->willReturn(true);
+
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookMessage->expects($this->exactly(2))
+ ->method('getEventTopic')
+ ->willReturn($eventTopic);
+ $webhookMessage->expects($this->once())
+ ->method('getData')
+ ->willReturn($data);
+ $this->webhookMessageReader->expects($this->once())
+ ->method('read')
+ ->with($this->webhookRequest)
+ ->willReturn($webhookMessage);
+
+ $caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->caseRepository->expects(self::once())
+ ->method('getByCaseId')
+ ->with(self::equalTo($caseId))
+ ->willReturn($caseEntity);
+
+ $caseUpdatingService = $this->getMockBuilder(UpdatingService::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $caseUpdatingService->expects($this->once())
+ ->method('update')
+ ->with($caseEntity, $data);
+
+ $this->caseUpdatingServiceFactory->expects($this->once())
+ ->method('create')
+ ->with($eventTopic)
+ ->willReturn($caseUpdatingService);
+
+ $this->controller->execute();
+ }
+
+ /**
+ * Case when there is exception while updating case
+ */
+ public function testExecuteCaseUpdatingServiceException()
+ {
+ $eventTopic = 'cases/creation';
+ $caseId = 1;
+ $data = ['score' => 200, 'caseId' => $caseId];
+
+ $this->webhookRequestValidator->expects($this->once())
+ ->method('validate')
+ ->willReturn(true);
+
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookMessage->expects($this->exactly(2))
+ ->method('getEventTopic')
+ ->willReturn($eventTopic);
+ $webhookMessage->expects($this->once())
+ ->method('getData')
+ ->willReturn($data);
+ $this->webhookMessageReader->expects($this->once())
+ ->method('read')
+ ->with($this->webhookRequest)
+ ->willReturn($webhookMessage);
+
+ $caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->caseRepository->expects(self::once())
+ ->method('getByCaseId')
+ ->with(self::equalTo($caseId))
+ ->willReturn($caseEntity);
+
+ $caseUpdatingService = $this->getMockBuilder(UpdatingService::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $caseUpdatingService->expects($this->once())
+ ->method('update')
+ ->with($caseEntity, $data)
+ ->willThrowException(new LocalizedException(__('Error')));
+
+ $this->caseUpdatingServiceFactory->expects($this->once())
+ ->method('create')
+ ->with($eventTopic)
+ ->willReturn($caseUpdatingService);
+
+ $this->response->expects($this->once())
+ ->method('setHttpResponseCode')
+ ->with(400);
+ $this->logger->expects($this->once())
+ ->method('critical');
+
+ $this->controller->execute();
+ }
+
+ /**
+ * Case when webhook request validation fails
+ */
+ public function testExecuteRequestValidationFails()
+ {
+ $this->webhookRequestValidator->expects($this->once())
+ ->method('validate')
+ ->willReturn(false);
+ $this->redirect->expects($this->once())
+ ->method('redirect')
+ ->with($this->response, 'noroute', []);
+ $this->webhookMessageReader->expects($this->never())
+ ->method('read');
+ $this->caseUpdatingServiceFactory->expects($this->never())
+ ->method('create');
+
+ $this->controller->execute();
+ }
+
+ /**
+ * Case when webhook request has test event topic.
+ */
+ public function testExecuteWithTestEventTopic()
+ {
+ $this->webhookRequestValidator->expects($this->once())
+ ->method('validate')
+ ->willReturn(true);
+ $this->redirect->expects($this->never())
+ ->method('redirect');
+
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookMessage->expects($this->once())
+ ->method('getEventTopic')
+ ->willReturn('cases/test');
+ $webhookMessage->expects($this->never())
+ ->method('getData');
+
+ $this->webhookMessageReader->expects($this->once())
+ ->method('read')
+ ->with($this->webhookRequest)
+ ->willReturn($webhookMessage);
+
+ $this->caseUpdatingServiceFactory->expects($this->never())
+ ->method('create');
+
+ $this->controller->execute();
+ }
+
+ /**
+ * Checks a test case when received input data does not contain Signifyd case id.
+ *
+ * @covers \Magento\Signifyd\Controller\Webhooks\Handler::execute
+ */
+ public function testExecuteWithMissedCaseId()
+ {
+ $this->webhookRequestValidator->expects(self::once())
+ ->method('validate')
+ ->willReturn(true);
+
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookMessage->expects($this->once())
+ ->method('getEventTopic')
+ ->willReturn('cases/creation');
+ $webhookMessage->expects(self::once())
+ ->method('getData')
+ ->willReturn([
+ 'orderId' => '1000101'
+ ]);
+
+ $this->webhookMessageReader->expects(self::once())
+ ->method('read')
+ ->with($this->webhookRequest)
+ ->willReturn($webhookMessage);
+
+ $this->redirect->expects(self::once())
+ ->method('redirect')
+ ->with($this->response, 'noroute', []);
+
+ $this->controller->execute();
+ }
+
+ /**
+ * Checks a case when Signifyd case entity not found.
+ *
+ * @covers \Magento\Signifyd\Controller\Webhooks\Handler::execute
+ */
+ public function testExecuteWithNotFoundCaseEntity()
+ {
+ $caseId = 123;
+
+ $this->webhookRequestValidator->expects(self::once())
+ ->method('validate')
+ ->willReturn(true);
+
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData'])
+ ->getMock();
+ $webhookMessage->expects(self::once())
+ ->method('getData')
+ ->willReturn([
+ 'orderId' => '1000101',
+ 'caseId' => $caseId
+ ]);
+
+ $this->webhookMessageReader->expects(self::once())
+ ->method('read')
+ ->with($this->webhookRequest)
+ ->willReturn($webhookMessage);
+
+ $this->caseRepository->expects(self::once())
+ ->method('getByCaseId')
+ ->with(self::equalTo($caseId))
+ ->willReturn(null);
+
+ $this->redirect->expects(self::once())
+ ->method('redirect')
+ ->with($this->response, 'noroute', []);
+
+ $this->controller->execute();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceFactoryTest.php
new file mode 100644
index 0000000000000..f0184c032b550
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceFactoryTest.php
@@ -0,0 +1,167 @@
+config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isActive'])
+ ->getMock();
+
+ $this->fakeObjectManager = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMockForAbstractClass();
+
+ $this->generatorFactory = $this->getMockBuilder(GeneratorFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->factory = $objectManager->getObject(UpdatingServiceFactory::class, [
+ 'objectManager' => $this->fakeObjectManager,
+ 'generatorFactory' => $this->generatorFactory,
+ 'config' => $this->config
+ ]);
+ }
+
+ /**
+ * Checks type of instance for updating service if Signifyd is not enabled.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingServiceFactory::create
+ */
+ public function testCreateWithInactiveConfig()
+ {
+ $type = 'cases/creation';
+ $this->config->expects(self::once())
+ ->method('isActive')
+ ->willReturn(false);
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(StubUpdatingService::class)
+ ->willReturn(new StubUpdatingService());
+
+ $instance = $this->factory->create($type);
+ static::assertInstanceOf(StubUpdatingService::class, $instance);
+ }
+
+ /**
+ * Checks type of instance for updating service if test type is received.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingServiceFactory::create
+ */
+ public function testCreateWithTestType()
+ {
+ $type = 'cases/test';
+ $this->config->expects(self::once())
+ ->method('isActive')
+ ->willReturn(true);
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(StubUpdatingService::class)
+ ->willReturn(new StubUpdatingService());
+
+ $instance = $this->factory->create($type);
+ static::assertInstanceOf(StubUpdatingService::class, $instance);
+ }
+
+ /**
+ * Checks exception type and message for unknown case type.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingServiceFactory::create
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage Specified message type does not supported.
+ */
+ public function testCreateWithException()
+ {
+ $type = 'cases/unknown';
+ $this->config->expects(self::once())
+ ->method('isActive')
+ ->willReturn(true);
+
+ $this->generatorFactory->expects(self::once())
+ ->method('create')
+ ->with($type)
+ ->willThrowException(new \InvalidArgumentException('Specified message type does not supported.'));
+
+ $this->factory->create($type);
+ }
+
+ /**
+ * Checks if factory creates correct instance of case updating service.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingServiceFactory::create
+ */
+ public function testCreate()
+ {
+ $type = 'case/creation';
+ $this->config->expects(self::once())
+ ->method('isActive')
+ ->willReturn(true);
+
+ $messageGenerator = $this->getMockBuilder(GeneratorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->generatorFactory->expects(self::once())
+ ->method('create')
+ ->with($type)
+ ->willReturn($messageGenerator);
+
+ $service = $this->getMockBuilder(UpdatingService::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(UpdatingService::class, ['messageGenerator' => $messageGenerator])
+ ->willReturn($service);
+
+ $result = $this->factory->create($type);
+ static::assertInstanceOf(UpdatingService::class, $result);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceTest.php
new file mode 100644
index 0000000000000..6eb7e5c37d5fc
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/CaseServices/UpdatingServiceTest.php
@@ -0,0 +1,316 @@
+objectManager = new ObjectManager($this);
+
+ $this->messageGenerator = $this->getMockBuilder(GeneratorInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['generate'])
+ ->getMock();
+
+ $this->caseRepository = $this->getMockBuilder(CaseRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getByCaseId'])
+ ->getMockForAbstractClass();
+
+ $this->commentsHistoryUpdater = $this->getMockBuilder(CommentsHistoryUpdater::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['addComment'])
+ ->getMock();
+
+ $this->orderGridUpdater = $this->getMockBuilder(OrderGridUpdater::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->orderStateService = $this->getMockBuilder(OrderStateService::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->service = $this->objectManager->getObject(UpdatingService::class, [
+ 'messageGenerator' => $this->messageGenerator,
+ 'caseRepository' => $this->caseRepository,
+ 'commentsHistoryUpdater' => $this->commentsHistoryUpdater,
+ 'orderGridUpdater' => $this->orderGridUpdater,
+ 'orderStateService' => $this->orderStateService
+ ]);
+ }
+
+ /**
+ * Checks a test case when Signifyd case is empty entity.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage The case entity should not be empty.
+ */
+ public function testUpdateWithEmptyCaseEntity()
+ {
+ $data = [];
+ $caseEntity = $this->withCaseEntity(null, 123, $data);
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Checks a test case when Signifyd case id is not specified for a case entity.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage The case entity should not be empty.
+ */
+ public function testUpdateWithEmptyCaseId()
+ {
+ $data = [
+ 'caseId' => 123
+ ];
+ $caseEntity = $this->withCaseEntity(1, null, $data);
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Checks as test case when service cannot save Signifyd case entity
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage Cannot update Case entity.
+ */
+ public function testUpdateWithFailedCaseSaving()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'status' => CaseInterface::STATUS_OPEN,
+ 'orderId' => '10000012',
+ 'score' => 500
+ ];
+
+ $caseEntity = $this->withCaseEntity(1, $caseId, $data);
+
+ $this->caseRepository->expects(self::once())
+ ->method('save')
+ ->willThrowException(new \Exception('Something wrong.'));
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Checks as test case when message generator throws an exception
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage Cannot update Case entity.
+ */
+ public function testUpdateWithExceptionFromMessageGenerator()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId
+ ];
+
+ $caseEntity = $this->withCaseEntity(1, $caseId, $data);
+
+ $this->caseRepository->expects(self::never())
+ ->method('save')
+ ->with($caseEntity)
+ ->willReturn($caseEntity);
+
+ $this->messageGenerator->expects(self::once())
+ ->method('generate')
+ ->with($data)
+ ->willThrowException(new GeneratorException(__('Cannot generate message.')));
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Checks a test case when comments history updater throws an exception.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @expectedExceptionMessage Cannot update Case entity.
+ */
+ public function testUpdateWithFailedCommentSaving()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'orderId' => 1
+ ];
+
+ $caseEntity = $this->withCaseEntity(1, $caseId, $data);
+
+ $this->caseRepository->expects(self::once())
+ ->method('save')
+ ->with($caseEntity)
+ ->willReturn($caseEntity);
+
+ $this->orderGridUpdater->expects(self::once())
+ ->method('update')
+ ->with($data['orderId']);
+
+ $message = __('Message is generated.');
+ $this->messageGenerator->expects(self::once())
+ ->method('generate')
+ ->with($data)
+ ->willReturn($message);
+
+ $this->commentsHistoryUpdater->expects(self::once())
+ ->method('addComment')
+ ->with($caseEntity, $message)
+ ->willThrowException(new \Exception('Something wrong'));
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Checks a test case when Signifyd case entity is successfully updated and message stored in comments history.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ */
+ public function testUpdate()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'orderId' => 1
+ ];
+
+ $caseEntity = $this->withCaseEntity(21, $caseId, $data);
+
+ $caseEntitySaved = clone $caseEntity;
+ $caseEntitySaved->expects(self::once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn('APPROVED');
+
+ $this->caseRepository->expects(self::once())
+ ->method('save')
+ ->with($caseEntity)
+ ->willReturn($caseEntitySaved);
+
+ $message = __('Message is generated.');
+ $this->messageGenerator->expects(self::once())
+ ->method('generate')
+ ->with($data)
+ ->willReturn($message);
+
+ $this->orderGridUpdater->expects(self::once())
+ ->method('update')
+ ->with($data['orderId']);
+
+ $this->commentsHistoryUpdater->expects(self::once())
+ ->method('addComment')
+ ->with($caseEntitySaved, $message);
+
+ $this->orderStateService->expects(self::once())
+ ->method('updateByCase')
+ ->with($caseEntitySaved);
+
+ $this->service->update($caseEntity, $data);
+ }
+
+ /**
+ * Create mock for case entity with common scenarios.
+ *
+ * @param $caseEntityId
+ * @param $caseId
+ * @param array $data
+ * @return CaseInterface|MockObject
+ */
+ private function withCaseEntity($caseEntityId, $caseId, array $data = [])
+ {
+ /** @var CaseInterface|MockObject $caseEntity */
+ $caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods([
+ 'getEntityId', 'getCaseId', 'getOrderId',
+ 'setCaseId', 'setStatus', 'setOrderId', 'setScore'
+ ])
+ ->getMockForAbstractClass();
+
+ $caseEntity->expects(self::any())
+ ->method('getEntityId')
+ ->willReturn($caseEntityId);
+ $caseEntity->expects(self::any())
+ ->method('getCaseId')
+ ->willReturn($caseId);
+
+ foreach ($data as $property => $value) {
+ $method = 'set' . ucfirst($property);
+ if ($property === 'orderId') {
+ $caseEntity->expects(self::never())
+ ->method($method);
+ }
+ $caseEntity->expects(self::any())
+ ->method($method)
+ ->with(self::equalTo($value))
+ ->willReturnSelf();
+
+ $method = 'get' . ucfirst($property);
+ $caseEntity->expects(self::any())
+ ->method($method)
+ ->willReturn($value);
+ }
+
+ return $caseEntity;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/CommentsHistoryUpdaterTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/CommentsHistoryUpdaterTest.php
new file mode 100644
index 0000000000000..5cbb3d8d93cdd
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/CommentsHistoryUpdaterTest.php
@@ -0,0 +1,176 @@
+historyFactory = $this->getMockBuilder(HistoryFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create', 'save'])
+ ->getMock();
+
+ $this->historyRepository = $this->getMockBuilder(OrderStatusHistoryRepositoryInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getOrderId'])
+ ->getMockForAbstractClass();
+
+ $this->initCommentMock();
+
+ $this->updater = $objectManager->getObject(CommentsHistoryUpdater::class, [
+ 'historyFactory' => $this->historyFactory,
+ 'historyRepository' => $this->historyRepository
+ ]);
+ }
+
+ /**
+ * Checks a test case when updater throws an exception while saving history comment.
+ *
+ * @covers \Magento\Signifyd\Model\CommentsHistoryUpdater::addComment
+ * @expectedException \Exception
+ */
+ public function testAddCommentWithException()
+ {
+ $this->caseEntity->expects(self::once())
+ ->method('getOrderId')
+ ->willReturn(self::$orderId);
+
+ $this->historyEntity->method('setStatus')
+ ->with('')
+ ->willReturnSelf();
+ $this->historyRepository->expects(self::once())
+ ->method('save')
+ ->with($this->historyEntity)
+ ->willThrowException(new \Exception('Cannot save comment message.'));
+
+ $this->updater->addComment($this->caseEntity, __(self::$message));
+ }
+
+ /**
+ * Checks a test case when updater successfully saves history comment.
+ *
+ * @covers \Magento\Signifyd\Model\CommentsHistoryUpdater::addComment
+ */
+ public function testAddComment()
+ {
+ $this->caseEntity->expects(self::once())
+ ->method('getOrderId')
+ ->willReturn(self::$orderId);
+
+ $this->historyEntity->method('setStatus')
+ ->with(self::$status)
+ ->willReturnSelf();
+ $this->historyRepository->expects(self::once())
+ ->method('save')
+ ->with($this->historyEntity)
+ ->willReturnSelf();
+
+ $this->updater->addComment($this->caseEntity, __(self::$message), self::$status);
+ }
+
+ /**
+ * Checks a test when message does not specified.
+ *
+ * @covers \Magento\Signifyd\Model\CommentsHistoryUpdater::addComment
+ */
+ public function testAddCommentWithoutMessage()
+ {
+ $this->caseEntity->expects(self::never())
+ ->method('getOrderId');
+
+ $this->historyFactory->expects(self::never())
+ ->method('save');
+
+ $phrase = '';
+ $this->updater->addComment($this->caseEntity, __($phrase));
+ }
+
+ /**
+ * Creates mock object for history entity.
+ *
+ * @return void
+ */
+ private function initCommentMock()
+ {
+ $this->historyEntity = $this->getMockBuilder(OrderStatusHistoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setParentId', 'setComment', 'setEntityName', 'save'])
+ ->getMockForAbstractClass();
+
+ $this->historyFactory->method('create')
+ ->willReturn($this->historyEntity);
+
+ $this->historyEntity->method('setParentId')
+ ->with(self::$orderId)
+ ->willReturnSelf();
+ $this->historyEntity->method('setComment')
+ ->with(self::$message)
+ ->willReturnSelf();
+ $this->historyEntity->method('setEntityName')
+ ->with('order')
+ ->willReturnSelf();
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/CustomerOrdersTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/CustomerOrdersTest.php
new file mode 100644
index 0000000000000..02d3b4b9ad7a7
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/CustomerOrdersTest.php
@@ -0,0 +1,267 @@
+objectManager = new ObjectManager($this);
+
+ $this->orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->filterBuilder = $this->getMockBuilder(FilterBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = $this->objectManager->getObject(CustomerOrders::class, [
+ 'filterBuilder' => $this->filterBuilder,
+ 'orderRepository' => $this->orderRepository,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilder,
+ 'logger' => $this->logger
+ ]);
+
+ $this->initCurrencies();
+ $this->initOrderRepository();
+
+ $this->objectManager->setBackwardCompatibleProperty(
+ $this->model,
+ 'currencies',
+ ['EUR' => $this->eurCurrency, 'UAH' => $this->uahCurrency]
+ );
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CustomerOrders::getAggregatedOrdersInfo()
+ */
+ public function testGetCountAndTotalAmount()
+ {
+ $this->eurCurrency->expects($this->once())
+ ->method('convert')
+ ->with(self::$eurAmount, 'USD')
+ ->willReturn(109);
+
+ $this->uahCurrency->expects($this->once())
+ ->method('convert')
+ ->with(self::$uahAmount, 'USD')
+ ->willReturn(10.35);
+
+ $actual = $this->model->getAggregatedOrdersInfo(self::$customerId);
+
+ static::assertEquals(3, $actual['aggregateOrderCount']);
+ static::assertEquals(169.35, $actual['aggregateOrderDollars']);
+ }
+
+ /**
+ * Test case when required currency rate is absent and exception is thrown
+ * @covers \Magento\Signifyd\Model\CustomerOrders::getAggregatedOrdersInfo()
+ */
+ public function testGetCountAndTotalAmountNegative()
+ {
+ $this->eurCurrency->expects($this->once())
+ ->method('convert')
+ ->with(self::$eurAmount, 'USD')
+ ->willReturn(109);
+
+ $this->uahCurrency->expects($this->once())
+ ->method('convert')
+ ->with(self::$uahAmount, 'USD')
+ ->willThrowException(new \Exception());
+
+ $this->logger->expects($this->once())
+ ->method('error');
+
+ $actual = $this->model->getAggregatedOrdersInfo(self::$customerId);
+
+ $this->assertNull($actual['aggregateOrderCount']);
+ $this->assertNull($actual['aggregateOrderDollars']);
+ }
+
+ /**
+ * Populate order repository with mocked orders
+ */
+ private function initOrderRepository()
+ {
+ $this->filterBuilder->expects($this->once())
+ ->method('setField')
+ ->willReturnSelf();
+ $this->filterBuilder->expects($this->once())
+ ->method('setValue')
+ ->willReturnSelf();
+ $filter = $this->getMockBuilder(\Magento\Framework\Api\Filter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->filterBuilder->expects($this->once())
+ ->method('create')
+ ->willReturn($filter);
+
+ $searchCriteria = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteria::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->searchCriteriaBuilder->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteria);
+
+ $orderSearchResult = $this->getMockBuilder(OrderSearchResultInterface::class)
+ ->getMockForAbstractClass();
+ $orderSearchResult->expects($this->once())
+ ->method('getItems')
+ ->willReturn($this->getOrders());
+ $this->orderRepository->expects($this->once())
+ ->method('getList')
+ ->willReturn($orderSearchResult);
+ }
+
+ /**
+ * Creates mocks for currencies
+ * @return void
+ */
+ private function initCurrencies()
+ {
+ $this->eurCurrency = $this->getMockBuilder(Currency::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['convert'])
+ ->getMock();
+
+ $this->uahCurrency = $this->getMockBuilder(Currency::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['convert'])
+ ->getMock();
+ }
+
+ /**
+ * Get list of mocked orders with different currencies
+ * @return array
+ */
+ private function getOrders()
+ {
+ $eurOrder = $this->getMockBuilder(Order::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBaseGrandTotal', 'getBaseCurrencyCode'])
+ ->getMock();
+
+ $eurOrder->expects($this->once())
+ ->method('getBaseGrandTotal')
+ ->willReturn(self::$eurAmount);
+ $eurOrder->expects($this->once())
+ ->method('getBaseCurrencyCode')
+ ->willReturn('EUR');
+
+ $uahOrder = $this->getMockBuilder(Order::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBaseGrandTotal', 'getBaseCurrencyCode'])
+ ->getMock();
+
+ $uahOrder->expects($this->once())
+ ->method('getBaseGrandTotal')
+ ->willReturn(self::$uahAmount);
+ $uahOrder->expects($this->once())
+ ->method('getBaseCurrencyCode')
+ ->willReturn('UAH');
+
+ $usdOrder = $this->getMockBuilder(Order::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBaseGrandTotal', 'getBaseCurrencyCode'])
+ ->getMock();
+
+ $usdOrder->expects($this->once())
+ ->method('getBaseGrandTotal')
+ ->willReturn(self::$usdAmount);
+ $usdOrder->expects($this->once())
+ ->method('getBaseCurrencyCode')
+ ->willReturn('USD');
+
+ return [$usdOrder, $eurOrder, $uahOrder];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelGuaranteeAbilityTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelGuaranteeAbilityTest.php
new file mode 100644
index 0000000000000..f7b4e473a0ec8
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelGuaranteeAbilityTest.php
@@ -0,0 +1,154 @@
+orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->caseManagement = $this->getMockBuilder(CaseManagement::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cancelGuaranteeAbility = new CancelGuaranteeAbility(
+ $this->caseManagement,
+ $this->orderRepository
+ );
+ }
+
+ /**
+ * Success test for Cancel Guarantee Request button
+ */
+ public function testIsAvailableSuccess()
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $case->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseEntity::GUARANTEE_APPROVED);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ /** @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject $order */
+ $order = $this->getMockBuilder(OrderInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willReturn($order);
+
+ $this->assertTrue($this->cancelGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when Case entity doesn't exist for order
+ */
+ public function testIsAvailableWithNullCase()
+ {
+ $orderId = 123;
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn(null);
+
+ $this->assertFalse($this->cancelGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when Guarantee Disposition has Canceled states.
+ */
+ public function testIsAvailableWithCanceledGuarantee()
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $case->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseEntity::GUARANTEE_CANCELED);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ $this->assertFalse($this->cancelGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when order does not exist.
+ */
+ public function testIsAvailableWithNullOrder()
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $case->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseEntity::GUARANTEE_APPROVED);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willThrowException(new NoSuchEntityException());
+
+ $this->assertFalse($this->cancelGuaranteeAbility->isAvailable($orderId));
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelingServiceTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelingServiceTest.php
new file mode 100644
index 0000000000000..f8f1d4a4522c9
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CancelingServiceTest.php
@@ -0,0 +1,215 @@
+caseManagement = $this->getMockBuilder(CaseManagementInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getByOrderId'])
+ ->getMockForAbstractClass();
+
+ $this->updatingFactory = $this->getMockBuilder(UpdatingServiceFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $this->gateway = $this->getMockBuilder(Gateway::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['cancelGuarantee'])
+ ->getMock();
+
+ $this->guaranteeAbility = $this->getMockBuilder(CancelGuaranteeAbility::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAvailable'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['error'])
+ ->getMockForAbstractClass();
+
+ $this->service = new CancelingService(
+ $this->caseManagement,
+ $this->updatingFactory,
+ $this->gateway,
+ $this->guaranteeAbility,
+ $this->logger
+ );
+ }
+
+ /**
+ * Checks a test case, when validation for a guarantee is failed.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ */
+ public function testCancelForOrderWithUnavailableDisposition()
+ {
+ $this->guaranteeAbility->expects(self::once())
+ ->method('isAvailable')
+ ->with(self::equalTo(self::$orderId))
+ ->willReturn(false);
+
+ $this->caseManagement->expects(self::never())
+ ->method('getByOrderId');
+
+ $this->gateway->expects(self::never())
+ ->method('cancelGuarantee');
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $this->updatingFactory->expects(self::never())
+ ->method('create');
+
+ $result = $this->service->cancelForOrder(self::$orderId);
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when request to Signifyd API fails.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ */
+ public function testCancelForOrderWithFailedRequest()
+ {
+ $this->withCaseEntity();
+
+ $this->gateway->expects(self::once())
+ ->method('cancelGuarantee')
+ ->with(self::equalTo(self::$caseId))
+ ->willThrowException(new GatewayException('Something wrong.'));
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with(self::equalTo('Something wrong.'));
+
+ $this->updatingFactory->expects(self::never())
+ ->method('create');
+
+ $result = $this->service->cancelForOrder(self::$orderId);
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when request to Signifyd successfully processed and case entity has been updated.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ */
+ public function testCancelForOrder()
+ {
+ $case = $this->withCaseEntity();
+
+ $this->gateway->expects(self::once())
+ ->method('cancelGuarantee')
+ ->with(self::equalTo(self::$caseId))
+ ->willReturn(CaseInterface::GUARANTEE_CANCELED);
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $service = $this->getMockBuilder(StubUpdatingService::class)
+ ->setMethods(['update'])
+ ->getMock();
+ $this->updatingFactory->expects(self::once())
+ ->method('create')
+ ->willReturn($service);
+
+ $service->expects(self::once())
+ ->method('update')
+ ->with(self::equalTo($case), self::equalTo(['guaranteeDisposition' => CaseInterface::GUARANTEE_CANCELED]));
+
+ $result = $this->service->cancelForOrder(self::$orderId);
+ self::assertTrue($result);
+ }
+
+ /**
+ * Gets mock for a case entity.
+ *
+ * @return CaseInterface|MockObject
+ */
+ private function withCaseEntity()
+ {
+ $this->guaranteeAbility->expects(self::once())
+ ->method('isAvailable')
+ ->with(self::equalTo(self::$orderId))
+ ->willReturn(true);
+
+ $caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getCaseId'])
+ ->getMockForAbstractClass();
+
+ $this->caseManagement->expects(self::once())
+ ->method('getByOrderId')
+ ->with(self::equalTo(self::$orderId))
+ ->willReturn($caseEntity);
+
+ $caseEntity->expects(self::once())
+ ->method('getCaseId')
+ ->willReturn(self::$caseId);
+ return $caseEntity;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php
new file mode 100644
index 0000000000000..7ba3ab3eef4f6
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php
@@ -0,0 +1,257 @@
+dateTimeFactory = new DateTimeFactory();
+ $this->orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMockForAbstractClass();
+ $this->caseManagement = $this->getMockBuilder(CaseManagement::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->createGuaranteeAbility = new CreateGuaranteeAbility(
+ $this->caseManagement,
+ $this->orderRepository,
+ $this->dateTimeFactory
+ );
+ }
+
+ public function testIsAvailableSuccess()
+ {
+ $orderId = 123;
+ $orderCreatedAt = $this->getDateAgo(6);
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $case->expects($this->once())
+ ->method('isGuaranteeEligible')
+ ->willReturn(true);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ /** @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject $order */
+ $order = $this->getMockBuilder(OrderInterface::class)
+ ->getMockForAbstractClass();
+ $order->expects($this->once())
+ ->method('getState')
+ ->willReturn(Order::STATE_COMPLETE);
+ $order->expects($this->once())
+ ->method('getCreatedAt')
+ ->willReturn($orderCreatedAt);
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willReturn($order);
+
+ $this->assertTrue($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when Case entity doesn't exist for order
+ */
+ public function testIsAvailableWithNullCase()
+ {
+ $orderId = 123;
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn(null);
+
+ $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when GuaranteeEligible for Case is false
+ */
+ public function testIsAvailableWithGuarantyEligibleFalse()
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $case->expects($this->once())
+ ->method('isGuaranteeEligible')
+ ->willReturn(false);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when GuaranteeEligible for Case is false
+ */
+ public function testIsAvailableWithNullOrder()
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $case->expects($this->once())
+ ->method('isGuaranteeEligible')
+ ->willReturn(true);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willThrowException(new NoSuchEntityException());
+
+ $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Tests case when order has Canceled Or Closed states.
+ *
+ * @param string $state
+ * @dataProvider isAvailableWithCanceledOrderDataProvider
+ */
+ public function testIsAvailableWithCanceledOrder($state)
+ {
+ $orderId = 123;
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $case->expects($this->once())
+ ->method('isGuaranteeEligible')
+ ->willReturn(true);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ /** @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject $order */
+ $order = $this->getMockBuilder(OrderInterface::class)
+ ->getMockForAbstractClass();
+ $order->expects($this->once())
+ ->method('getState')
+ ->willReturn($state);
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willReturn($order);
+
+ $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ public function isAvailableWithCanceledOrderDataProvider()
+ {
+ return [
+ [Order::STATE_CANCELED], [Order::STATE_CLOSED]
+ ];
+ }
+
+ public function testIsAvailableWithOldOrder()
+ {
+ $orderId = 123;
+ $orderCreatedAt = $this->getDateAgo(8);
+
+ /** @var CaseInterface|\PHPUnit_Framework_MockObject_MockObject $case */
+ $case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $case->expects($this->once())
+ ->method('isGuaranteeEligible')
+ ->willReturn(true);
+
+ $this->caseManagement->expects($this->once())
+ ->method('getByOrderId')
+ ->with($orderId)
+ ->willReturn($case);
+
+ /** @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject $order */
+ $order = $this->getMockBuilder(OrderInterface::class)
+ ->getMockForAbstractClass();
+ $order->expects($this->once())
+ ->method('getState')
+ ->willReturn(Order::STATE_COMPLETE);
+ $order->expects($this->once())
+ ->method('getCreatedAt')
+ ->willReturn($orderCreatedAt);
+
+ $this->orderRepository->expects($this->once())
+ ->method('get')
+ ->with($orderId)
+ ->willReturn($order);
+
+ $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId));
+ }
+
+ /**
+ * Returns date N days ago
+ *
+ * @param int $days number of days that will be deducted from the current date
+ * @return string
+ */
+ private function getDateAgo($days)
+ {
+ $createdAtTime = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
+ $createdAtTime->sub(new \DateInterval('P' . $days . 'D'));
+
+ return $createdAtTime->format('Y-m-d h:i:s');
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php
new file mode 100644
index 0000000000000..a22bfe12222a6
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php
@@ -0,0 +1,242 @@
+caseManagement = $this->getMockBuilder(CaseManagementInterface::class)
+ ->getMockForAbstractClass();
+
+ $caseUpdatingServiceFactory = $this->getMockBuilder(UpdatingServiceFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->caseUpdatingService = $this->getMockBuilder(UpdatingServiceInterface::class)
+ ->getMockForAbstractClass();
+ $caseUpdatingServiceFactory
+ ->method('create')
+ ->willReturn($this->caseUpdatingService);
+
+ $this->gateway = $this->getMockBuilder(Gateway::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->createGuaranteeAbility = $this->getMockBuilder(CreateGuaranteeAbility::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAvailable'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->service = new CreationService(
+ $this->caseManagement,
+ $caseUpdatingServiceFactory,
+ $this->gateway,
+ $this->createGuaranteeAbility,
+ $this->logger
+ );
+ }
+
+ /**
+ * Checks a test case, when guarantee ability checker does not allow to submit case for a guarantee.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CreationService::createForOrder
+ */
+ public function testCreateForOrderWithNotEligibleCase()
+ {
+ $orderId = 1;
+
+ $this->createGuaranteeAbility->expects(self::once())
+ ->method('isAvailable')
+ ->with($orderId)
+ ->willReturn(false);
+
+ $this->caseManagement->expects(self::never())
+ ->method('getByOrderId');
+
+ $this->gateway->expects(self::never())
+ ->method('submitCaseForGuarantee');
+
+ $result = $this->service->createForOrder($orderId);
+ self::assertFalse($result);
+ }
+
+ public function testCreateForOrderWitCase()
+ {
+ $dummyOrderId = 1;
+ $dummyCaseId = 42;
+ $this->withCaseEntityExistsForOrderId(
+ $dummyOrderId,
+ [
+ 'caseId' => $dummyCaseId,
+ ]
+ );
+
+ $this->gateway
+ ->expects($this->once())
+ ->method('submitCaseForGuarantee');
+
+ $this->service->createForOrder($dummyOrderId);
+ }
+
+ public function testCreateForOrderWithGatewayFailure()
+ {
+ $dummyOrderId = 1;
+ $dummyCaseId = 42;
+ $dummyGatewayFailureMessage = 'Everything fails sometimes';
+ $this->withCaseEntityExistsForOrderId(
+ $dummyOrderId,
+ [
+ 'caseId' => $dummyCaseId,
+ ]
+ );
+ $this->withGatewayFailure($dummyGatewayFailureMessage);
+
+ $this->logger
+ ->expects($this->once())
+ ->method('error')
+ ->with($this->equalTo($dummyGatewayFailureMessage));
+ $this->caseUpdatingService
+ ->expects($this->never())
+ ->method('update');
+
+ $result = $this->service->createForOrder($dummyOrderId);
+ $this->assertEquals(
+ false,
+ $result,
+ 'Service should return false in case of gateway failure'
+ );
+ }
+
+ public function testCreateForOrderWithGatewaySuccess()
+ {
+ $dummyOrderId = 1;
+ $dummyCaseId = 42;
+ $dummyGuaranteeDisposition = 'foo';
+ $caseEntity = $this->withCaseEntityExistsForOrderId(
+ $dummyOrderId,
+ [
+ 'caseId' => $dummyCaseId,
+ ]
+ );
+ $this->withGatewaySuccess($dummyGuaranteeDisposition);
+
+ $this->caseUpdatingService
+ ->expects($this->once())
+ ->method('update')
+ ->with($caseEntity, $this->equalTo([
+ 'caseId' => $dummyCaseId,
+ 'guaranteeDisposition' => $dummyGuaranteeDisposition,
+ ]));
+
+ $this->service->createForOrder($dummyOrderId);
+ }
+
+ public function testCreateForOrderWithCaseUpdate()
+ {
+ $dummyOrderId = 1;
+ $dummyCaseId = 42;
+ $dummyGuaranteeDisposition = 'foo';
+ $this->withCaseEntityExistsForOrderId(
+ $dummyOrderId,
+ [
+ 'caseId' => $dummyCaseId,
+ ]
+ );
+ $this->withGatewaySuccess($dummyGuaranteeDisposition);
+
+ $result = $this->service->createForOrder($dummyOrderId);
+ $this->assertEquals(
+ true,
+ $result,
+ 'Service should return true in case if case update service is called'
+ );
+ }
+
+ private function withCaseEntityExistsForOrderId($orderId, array $caseData = [])
+ {
+ $this->createGuaranteeAbility->expects(self::once())
+ ->method('isAvailable')
+ ->with(self::equalTo($orderId))
+ ->willReturn(true);
+
+ $dummyCaseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->getMockForAbstractClass();
+ foreach ($caseData as $caseProperty => $casePropertyValue) {
+ $dummyCaseEntity
+ ->method('get' . ucfirst($caseProperty))
+ ->willReturn($casePropertyValue);
+ }
+
+ $this->caseManagement
+ ->method('getByOrderId')
+ ->with($this->equalTo($orderId))
+ ->willReturn($dummyCaseEntity);
+
+ return $dummyCaseEntity;
+ }
+
+ private function withGatewayFailure($failureMessage)
+ {
+ $this->gateway
+ ->method('submitCaseForGuarantee')
+ ->willThrowException(new GatewayException($failureMessage));
+ }
+
+ private function withGatewaySuccess($gatewayResult)
+ {
+ $this->gateway
+ ->method('submitCaseForGuarantee')
+ ->willReturn($gatewayResult);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/CaseRescoreTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/CaseRescoreTest.php
new file mode 100644
index 0000000000000..ba14036cd68d0
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/CaseRescoreTest.php
@@ -0,0 +1,148 @@
+ 100,
+ 'score' => 200
+ ];
+
+ /**
+ * @var ObjectManager
+ */
+ private $objectManager;
+
+ /**
+ * @var CaseRepositoryInterface|MockObject
+ */
+ private $caseRepository;
+
+ /**
+ * @var CaseRescore|MockObject
+ */
+ private $caseRescore;
+
+ /**
+ * @var CaseInterface|MockObject
+ */
+ private $case;
+
+ /**
+ * @inheritdoc
+ */
+ protected function setUp()
+ {
+ $this->case = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManager = new ObjectManager($this);
+ $this->caseRepository = $this->getMockBuilder(CaseRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->caseRescore = $this->objectManager->getObject(CaseRescore::class, [
+ 'caseRepository' => $this->caseRepository
+ ]);
+ }
+
+ /**
+ * Data array without required attribute caseId.
+ *
+ * @expectedException \Magento\Signifyd\Model\MessageGenerators\GeneratorException
+ * @expectedExceptionMessage The "caseId" should not be empty
+ */
+ public function testGenerateEmptyCaseIdException()
+ {
+ $this->caseRescore->generate([]);
+ }
+
+ /**
+ * Case entity was not found in DB.
+ *
+ * @expectedException \Magento\Signifyd\Model\MessageGenerators\GeneratorException
+ * @expectedExceptionMessage Case entity not found.
+ */
+ public function testGenerateNotFoundException()
+ {
+ $this->caseRepository->expects($this->once())
+ ->method('getByCaseId')
+ ->with(self::$data['caseId'])
+ ->willReturn(null);
+
+ $this->caseRescore = $this->objectManager->getObject(CaseRescore::class, [
+ 'caseRepository' => $this->caseRepository
+ ]);
+
+ $this->caseRescore->generate(self::$data);
+ }
+
+ /**
+ * Generate case message with not empty previous score.
+ */
+ public function testGenerateWithPreviousScore()
+ {
+ $this->case->expects($this->once())
+ ->method('getScore')
+ ->willReturn(self::$data['score']);
+
+ $this->caseRepository->expects($this->once())
+ ->method('getByCaseId')
+ ->with(self::$data['caseId'])
+ ->willReturn($this->case);
+
+ $this->caseRescore = $this->objectManager->getObject(CaseRescore::class, [
+ 'caseRepository' => $this->caseRepository
+ ]);
+
+ $phrase = __(
+ 'Case Update: New score for the order is %1. Previous score was %2.',
+ self::$data['score'],
+ self::$data['score']
+ );
+
+ $message = $this->caseRescore->generate(self::$data);
+
+ $this->assertEquals($phrase, $message);
+ }
+
+ /**
+ * Generate case message with empty previous score.
+ */
+ public function testGenerateWithoutPreviousScore()
+ {
+ $this->caseRepository->expects($this->once())
+ ->method('getByCaseId')
+ ->with(self::$data['caseId'])
+ ->willReturn($this->case);
+
+ $this->caseRescore = $this->objectManager->getObject(CaseRescore::class, [
+ 'caseRepository' => $this->caseRepository
+ ]);
+
+ $phrase = __(
+ 'Case Update: New score for the order is %1. Previous score was %2.',
+ self::$data['score'],
+ null
+ );
+
+ $message = $this->caseRescore->generate(self::$data);
+
+ $this->assertEquals($phrase, $message);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/GeneratorFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/GeneratorFactoryTest.php
new file mode 100644
index 0000000000000..50f87df3b694f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/GeneratorFactoryTest.php
@@ -0,0 +1,99 @@
+fakeObjectManager = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMockForAbstractClass();
+
+ $this->factory = $objectManager->getObject(GeneratorFactory::class, [
+ 'objectManager' => $this->fakeObjectManager
+ ]);
+ }
+
+ /**
+ * Checks if factory returns correct instance of message generator.
+ *
+ * @covers \Magento\Signifyd\Model\MessageGenerators\GeneratorFactory::create
+ * @param string $type
+ * @param string $className
+ * @dataProvider typeDataProvider
+ */
+ public function testCreate($type, $className)
+ {
+ $generator = $this->getMockBuilder($className)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with($className)
+ ->willReturn($generator);
+
+ $instance = $this->factory->create($type);
+ self::assertInstanceOf($className, $instance);
+ }
+
+ /**
+ * Get list of available messages generators types and equal class names.
+ *
+ * @return array
+ */
+ public function typeDataProvider()
+ {
+ return [
+ ['cases/creation', PatternGenerator::class],
+ ['cases/review', PatternGenerator::class],
+ ['cases/rescore', CaseRescore::class],
+ ['guarantees/completion', PatternGenerator::class],
+ ['guarantees/creation', PatternGenerator::class],
+ ['guarantees/cancel', PatternGenerator::class],
+ ];
+ }
+
+ /**
+ * Checks correct exception message for unknown type of message generator.
+ *
+ * @covers \Magento\Signifyd\Model\MessageGenerators\GeneratorFactory::create
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage Specified message type does not supported.
+ */
+ public function testCreateWithException()
+ {
+ $type = 'cases/unknown';
+ $this->factory->create($type);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/PatternGeneratorTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/PatternGeneratorTest.php
new file mode 100644
index 0000000000000..9d5f71f657a1e
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/MessageGenerators/PatternGeneratorTest.php
@@ -0,0 +1,83 @@
+generate($data);
+ }
+
+ /**
+ * Checks cases with different template placeholders and input data.
+ *
+ * @covers \Magento\Signifyd\Model\MessageGenerators\PatternGenerator::generate
+ * @param string $template
+ * @param array $requiredFields
+ * @param string $expected
+ * @dataProvider messageDataProvider
+ */
+ public function testGenerate($template, array $requiredFields, $expected)
+ {
+ $data = [
+ 'caseId' => 123,
+ 'reviewDisposition' => 'Good',
+ 'guaranteeDisposition' => 'Approved',
+ 'score' => 500,
+ 'case_score' => 300
+ ];
+
+ $generator = new PatternGenerator($template, $requiredFields);
+ $actual = $generator->generate($data);
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * Get list of variations with message templates, required fields and expected generated messages.
+ *
+ * @return array
+ */
+ public function messageDataProvider()
+ {
+ return [
+ [
+ 'Signifyd Case %1 has been created for order.',
+ ['caseId'],
+ 'Signifyd Case 123 has been created for order.'
+ ],
+ [
+ 'Case Update: Case Review was completed. Review Deposition is %1.',
+ ['reviewDisposition'],
+ 'Case Update: Case Review was completed. Review Deposition is Good.'
+ ],
+ [
+ 'Case Update: New score for the order is %1. Previous score was %2.',
+ ['score', 'case_score'],
+ 'Case Update: New score for the order is 500. Previous score was 300.'
+ ],
+ [
+ 'Case Update: Case is submitted for guarantee.',
+ [],
+ 'Case Update: Case is submitted for guarantee.'
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/OrderStateServiceTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/OrderStateServiceTest.php
new file mode 100644
index 0000000000000..3a567a79891f8
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/OrderStateServiceTest.php
@@ -0,0 +1,204 @@
+orderManagement = $this->getMockBuilder(OrderManagementInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->commentsHistoryUpdater = $this->getMockBuilder(CommentsHistoryUpdater::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->orderFactory = $this->getMockBuilder(OrderFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->order = $this->getMockBuilder(Order::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->order->expects($this->once())
+ ->method('load')
+ ->willReturnSelf();
+
+ $this->orderFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($this->order);
+
+ $this->caseEntity = $this->getMockBuilder(CaseInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->caseEntity->expects($this->once())
+ ->method('getOrderId')
+ ->willReturn(self::$orderId);
+
+ $this->orderStateService = new OrderStateService(
+ $this->orderFactory,
+ $this->orderManagement,
+ $this->commentsHistoryUpdater
+ );
+ }
+
+ /**
+ * Tests update order state flow when case guarantee disposition is PENDING.
+ *
+ * @param bool $canHold
+ * @param bool $hold
+ * @param int $addCommentCall
+ * @dataProvider updateByCaseWithGuaranteePendingDataProvider
+ */
+ public function testUpdateByCaseWithGuaranteePending($canHold, $hold, $addCommentCall)
+ {
+ $this->caseEntity->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseInterface::GUARANTEE_PENDING);
+ $this->order->expects($this->any())
+ ->method('canHold')
+ ->willReturn($canHold);
+ $this->orderManagement->expects($this->any())
+ ->method('hold')
+ ->willReturn($hold);
+ $this->commentsHistoryUpdater->expects($this->exactly($addCommentCall))
+ ->method('addComment')
+ ->with(
+ $this->caseEntity,
+ __('Awaiting the Signifyd guarantee disposition.'),
+ Order::STATE_HOLDED
+ );
+
+ $this->orderStateService->updateByCase($this->caseEntity);
+ }
+
+ /**
+ * @return array
+ */
+ public function updateByCaseWithGuaranteePendingDataProvider()
+ {
+ return [
+ ['canHold' => true, 'hold' => true, 'addCommentCall' => 1],
+ ['canHold' => false, 'hold' => true, 'addCommentCall' => 0],
+ ['canHold' => true, 'hold' => false, 'addCommentCall' => 0],
+ ];
+ }
+
+ /**
+ * Tests update order state flow when case guarantee disposition is APPROVED.
+ *
+ * @param bool $canUnhold
+ * @param int $unholdCall
+ * @dataProvider updateByCaseWithGuaranteeApprovedDataProvider
+ */
+ public function testUpdateByCaseWithGuaranteeApproved($canUnhold, $unholdCall)
+ {
+ $this->caseEntity->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseInterface::GUARANTEE_APPROVED);
+ $this->order->expects($this->any())
+ ->method('canUnhold')
+ ->willReturn($canUnhold);
+ $this->orderManagement->expects($this->exactly($unholdCall))
+ ->method('unHold');
+ $this->commentsHistoryUpdater->expects($this->never())
+ ->method('addComment');
+
+ $this->orderStateService->updateByCase($this->caseEntity);
+ }
+
+ /**
+ * @return array
+ */
+ public function updateByCaseWithGuaranteeApprovedDataProvider()
+ {
+ return [
+ ['canUnhold' => true, 'unholdCall' => 1],
+ ['canUnhold' => false, 'unholdCall' => 0]
+ ];
+ }
+
+ /**
+ * Tests update order state flow when case guarantee disposition is DECLINED.
+ *
+ * @param bool $canHold
+ * @param int $holdCall
+ * @dataProvider updateByCaseWithGuaranteeDeclinedDataProvider
+ */
+ public function testUpdateByCaseWithGuaranteeDeclined($canHold, $holdCall)
+ {
+ $this->caseEntity->expects($this->once())
+ ->method('getGuaranteeDisposition')
+ ->willReturn(CaseInterface::GUARANTEE_DECLINED);
+ $this->order->expects($this->any())
+ ->method('canHold')
+ ->willReturn($canHold);
+ $this->orderManagement->expects($this->exactly($holdCall))
+ ->method('hold');
+ $this->commentsHistoryUpdater->expects($this->never())
+ ->method('addComment');
+
+ $this->orderStateService->updateByCase($this->caseEntity);
+ }
+
+ /**
+ * @return array
+ */
+ public function updateByCaseWithGuaranteeDeclinedDataProvider()
+ {
+ return [
+ ['canHold' => true, 'holdCall' => 1],
+ ['canHold' => false, 'holdCall' => 0]
+ ];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/XmlToArrayConfigConverterTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/XmlToArrayConfigConverterTest.php
new file mode 100644
index 0000000000000..319229e326c4b
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/XmlToArrayConfigConverterTest.php
@@ -0,0 +1,76 @@
+filePath = realpath(__DIR__) . '/_files/';
+
+ $objectManagerHelper = new ObjectManager($this);
+ $this->converter = $objectManagerHelper->getObject(
+ XmlToArrayConfigConverter::class
+ );
+ }
+
+ public function testConvert()
+ {
+ $testDom = $this->filePath . 'signifyd_payment_mapping.xml';
+ $dom = new \DOMDocument();
+ $dom->load($testDom);
+ $mapping = $this->converter->convert($dom);
+ $expectedArray = include $this->filePath . 'expected_array.php';
+
+ $this->assertEquals($expectedArray, $mapping);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Config\Dom\ValidationSchemaException
+ * @expectedExceptionMessage Only single entrance of "magento_code" node is required.
+ */
+ public function testConvertEmptyPaymentMethodException()
+ {
+ $dom = new \DOMDocument();
+ $element = $dom->createElement('payment_method');
+ $subelement = $dom->createElement('signifyd_code', 'test');
+ $element->appendChild($subelement);
+ $dom->appendChild($element);
+
+ $this->converter->convert($dom);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Config\Dom\ValidationSchemaException
+ * @expectedExceptionMessage Not empty value for "signifyd_code" node is required.
+ */
+ public function testConvertEmptySygnifydPaymentMethodException()
+ {
+ $dom = new \DOMDocument();
+ $element = $dom->createElement('payment_method');
+ $subelement = $dom->createElement('magento_code', 'test');
+ $subelement2 = $dom->createElement('signifyd_code', '');
+ $element->appendChild($subelement);
+ $element->appendChild($subelement2);
+ $dom->appendChild($element);
+
+ $this->converter->convert($dom);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/expected_array.php b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/expected_array.php
new file mode 100644
index 0000000000000..f5d3436ae6b7b
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/expected_array.php
@@ -0,0 +1,12 @@
+ 'PAYMENT_CARD',
+ 'payment_method_2' => 'PAYPAL_ACCOUNT',
+ 'payment_method_3' => 'CHECK',
+ 'payment_method_4' => 'CASH',
+ 'payment_method_5' => 'FREE'
+];
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/signifyd_payment_mapping.xml b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/signifyd_payment_mapping.xml
new file mode 100644
index 0000000000000..f70763e22c418
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentMethodMapper/_files/signifyd_payment_mapping.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ payment_method_1
+ PAYMENT_CARD
+
+
+ payment_method_2
+ PAYPAL_ACCOUNT
+
+
+ payment_method_3
+ CHECK
+
+
+ payment_method_4
+ CASH
+
+
+ payment_method_5
+ FREE
+
+
+
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/PaymentVerificationFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentVerificationFactoryTest.php
new file mode 100644
index 0000000000000..b0f9239d43bfa
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/PaymentVerificationFactoryTest.php
@@ -0,0 +1,222 @@
+objectManager = new ObjectManager($this);
+
+ $this->fakeObjectManager = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->config = $this->getMockBuilder(ConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->avsDefaultAdapter = $this->getMockBuilder(PaymentVerificationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cvvDefaultAdapter = $this->getMockBuilder(PaymentVerificationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->factory = $this->objectManager->getObject(PaymentVerificationFactory::class, [
+ 'objectManager' => $this->fakeObjectManager,
+ 'config' => $this->config,
+ 'avsDefaultAdapter' => $this->avsDefaultAdapter,
+ 'cvvDefaultAdapter' => $this->cvvDefaultAdapter
+ ]);
+ }
+
+ /**
+ * Checks a test case when factory creates CVV mapper for provided payment method.
+ *
+ * @covers \Magento\Signifyd\Model\PaymentVerificationFactory::createPaymentCvv
+ */
+ public function testCreatePaymentCvv()
+ {
+ $paymentMethodCode = 'exists_payment';
+
+ $this->config->expects(self::once())
+ ->method('setMethodCode')
+ ->with(self::equalTo($paymentMethodCode))
+ ->willReturnSelf();
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with('cvv_ems_adapter')
+ ->willReturn(PaymentVerificationInterface::class);
+
+ /** @var PaymentVerificationInterface|MockObject $cvvAdapter */
+ $cvvAdapter = $this->getMockBuilder(PaymentVerificationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(self::equalTo(PaymentVerificationInterface::class))
+ ->willReturn($cvvAdapter);
+
+ $mapper = $this->factory->createPaymentCvv($paymentMethodCode);
+ self::assertInstanceOf(PaymentVerificationInterface::class, $mapper);
+ }
+
+ /**
+ * Checks a test case, when provided payment method does not have cvv mapper.
+ *
+ * @covers \Magento\Signifyd\Model\PaymentVerificationFactory::createPaymentCvv
+ */
+ public function testCreateDefaultCvvMapper()
+ {
+ $paymentMethodCode = 'non_exists_payment';
+
+ $this->config->expects(self::once())
+ ->method('setMethodCode')
+ ->with(self::equalTo($paymentMethodCode))
+ ->willReturnSelf();
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with('cvv_ems_adapter')
+ ->willReturn(null);
+
+ $this->fakeObjectManager->expects(self::never())
+ ->method('create');
+
+ $mapper = $this->factory->createPaymentCvv($paymentMethodCode);
+ self::assertSame($this->cvvDefaultAdapter, $mapper);
+ }
+
+ /**
+ * Checks a test case, when mapper implementation does not corresponding to PaymentVerificationInterface.
+ *
+ * @covers \Magento\Signifyd\Model\PaymentVerificationFactory::createPaymentCvv
+ * @expectedException \Magento\Framework\Exception\ConfigurationMismatchException
+ * @expectedExceptionMessage stdClass must implement Magento\Payment\Api\PaymentVerificationInterface
+ */
+ public function testCreateWithUnsupportedImplementation()
+ {
+ $paymentMethodCode = 'exists_payment';
+
+ $this->config->expects(self::once())
+ ->method('setMethodCode')
+ ->with(self::equalTo($paymentMethodCode))
+ ->willReturnSelf();
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with('cvv_ems_adapter')
+ ->willReturn(\stdClass::class);
+
+ $cvvAdapter = new \stdClass();
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(self::equalTo(\stdClass::class))
+ ->willReturn($cvvAdapter);
+
+ $this->factory->createPaymentCvv($paymentMethodCode);
+ }
+
+ /**
+ * Checks a test case when factory creates AVS mapper for provided payment method.
+ *
+ * @covers \Magento\Signifyd\Model\PaymentVerificationFactory::createPaymentAvs
+ */
+ public function testCreatePaymentAvs()
+ {
+ $paymentMethodCode = 'exists_payment';
+
+ $this->config->expects(self::once())
+ ->method('setMethodCode')
+ ->with(self::equalTo($paymentMethodCode))
+ ->willReturnSelf();
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with('avs_ems_adapter')
+ ->willReturn(PaymentVerificationInterface::class);
+
+ $avsAdapter = $this->getMockBuilder(PaymentVerificationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fakeObjectManager->expects(self::once())
+ ->method('create')
+ ->with(self::equalTo(PaymentVerificationInterface::class))
+ ->willReturn($avsAdapter);
+
+ $mapper = $this->factory->createPaymentAvs($paymentMethodCode);
+ self::assertInstanceOf(PaymentVerificationInterface::class, $mapper);
+ }
+
+ /**
+ * Checks a test case when provided payment method does not support
+ */
+ public function testCreateDefaultAvsMapper()
+ {
+ $paymentMethodCode = 'non_exists_payment';
+
+ $this->config->expects(self::once())
+ ->method('setMethodCode')
+ ->with(self::equalTo($paymentMethodCode))
+ ->willReturnSelf();
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with('avs_ems_adapter')
+ ->willReturn(null);
+
+ $this->fakeObjectManager->expects(self::never())
+ ->method('create');
+
+ $mapper = $this->factory->createPaymentAvs($paymentMethodCode);
+ self::assertSame($this->avsDefaultAdapter, $mapper);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SalesOrderGrid/OrderGridUpdaterTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SalesOrderGrid/OrderGridUpdaterTest.php
new file mode 100644
index 0000000000000..885c9f018a488
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SalesOrderGrid/OrderGridUpdaterTest.php
@@ -0,0 +1,72 @@
+orderGrid = $this->getMockBuilder(GridInterface::class)
+ ->getMockForAbstractClass();
+ $this->globalConfig = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->model = new OrderGridUpdater($this->orderGrid, $this->globalConfig);
+ }
+
+ public function testUpdateInSyncMode()
+ {
+ $orderId = 1;
+
+ $this->globalConfig->expects($this->once())
+ ->method('getValue')
+ ->with('dev/grid/async_indexing', 'default', null)
+ ->willReturn(false);
+ $this->orderGrid->expects($this->once())
+ ->method('refresh')
+ ->with($orderId);
+
+ $this->model->update($orderId);
+ }
+
+ public function testUpdateInAsyncMode()
+ {
+ $orderId = 1;
+
+ $this->globalConfig->expects($this->once())
+ ->method('getValue')
+ ->with('dev/grid/async_indexing', 'default', null)
+ ->willReturn(true);
+ $this->orderGrid->expects($this->never())
+ ->method('refresh')
+ ->with($orderId);
+
+ $this->model->update($orderId);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php
new file mode 100644
index 0000000000000..776e8a75b9646
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php
@@ -0,0 +1,128 @@
+objectManager = new ObjectManager($this);
+
+ $this->config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->client = $this->getMockBuilder(ZendClient::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setHeaders', 'setMethod', 'setUri', 'setRawData'])
+ ->getMock();
+
+ $this->clientFactory = $this->getMockBuilder(ZendClientFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $this->clientFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($this->client);
+
+ $this->dataEncoder = $this->getMockBuilder(EncoderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClient = $this->objectManager->getObject(HttpClientFactory::class, [
+ 'config' => $this->config,
+ 'clientFactory' => $this->clientFactory,
+ 'dataEncoder' => $this->dataEncoder
+ ]);
+ }
+
+ public function testCreateHttpClient()
+ {
+ $this->config->expects($this->once())
+ ->method('getApiKey')
+ ->willReturn('testKey');
+
+ $this->config->expects($this->once())
+ ->method('getApiUrl')
+ ->willReturn('testUrl');
+
+ $client = $this->httpClient->create('url', 'method');
+
+ $this->assertInstanceOf(ZendClient::class, $client);
+ }
+
+ public function testCreateWithParams()
+ {
+ $param = ['id' => 1];
+ $json = '{"id":1}';
+
+ $this->config->expects($this->once())
+ ->method('getApiKey')
+ ->willReturn('testKey');
+
+ $this->config->expects($this->once())
+ ->method('getApiUrl')
+ ->willReturn(self::$dummy);
+
+ $this->dataEncoder->expects($this->once())
+ ->method('encode')
+ ->with($this->equalTo($param))
+ ->willReturn($json);
+
+ $this->client->expects($this->once())
+ ->method('setRawData')
+ ->with($this->equalTo($json), 'application/json')
+ ->willReturnSelf();
+
+ $client = $this->httpClient->create('url', 'method', $param);
+
+ $this->assertInstanceOf(ZendClient::class, $client);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php
new file mode 100644
index 0000000000000..bf0c6ee238d5f
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php
@@ -0,0 +1,179 @@
+objectManager = new ObjectManager($this);
+
+ $this->dataDecoder = $this->getMockBuilder(DecoderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->response = $this->getMockBuilder(Response::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStatus', 'getBody'])
+ ->getMock();
+
+ $this->responseHandler = $this->objectManager->getObject(ResponseHandler::class, [
+ 'dataDecoder' => $this->dataDecoder
+ ]);
+ }
+
+ /**
+ * @dataProvider errorsProvider
+ */
+ public function testHandleFailureMessage($code, $message)
+ {
+ $this->response->expects($this->any())
+ ->method('getStatus')
+ ->willReturn($code);
+
+ $this->response->expects($this->once())
+ ->method('getBody')
+ ->willReturn(self::$errorMessage);
+
+ try {
+ $this->responseHandler->handle($this->response);
+ } catch (ApiCallException $e) {
+ $this->assertEquals($e->getMessage(), sprintf($message, self::$errorMessage));
+ }
+ }
+
+ public function errorsProvider()
+ {
+ return [
+ [400, 'Bad Request - The request could not be parsed. Response: %s'],
+ [401, 'Unauthorized - user is not logged in, could not be authenticated. Response: %s'],
+ [403, 'Forbidden - Cannot access resource. Response: %s'],
+ [404, 'Not Found - resource does not exist. Response: %s'],
+ [
+ 409,
+ 'Conflict - with state of the resource on server. Can occur with (too rapid) PUT requests. Response: %s'
+ ],
+ [500, 'Server error. Response: %s']
+ ];
+ }
+
+ /**
+ * @expectedException \Magento\Signifyd\Model\SignifydGateway\ApiCallException
+ * @expectedExceptionMessage Response is not valid JSON: Decoding failed: Syntax error
+ */
+ public function testHandleEmptyJsonException()
+ {
+ $this->response->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(self::$successfulCode);
+
+ $this->response->expects($this->once())
+ ->method('getBody')
+ ->willReturn('');
+
+ $r = new \ReflectionObject($this->responseHandler);
+ $prop = $r->getProperty('phpVersionId');
+ $prop->setAccessible(true);
+ $prop->setValue(self::$phpVersionId);
+
+ $this->responseHandler->handle($this->response);
+ }
+
+ /**
+ * @expectedException \Magento\Signifyd\Model\SignifydGateway\ApiCallException
+ * @expectedExceptionMessage Response is not valid JSON: Some error
+ */
+ public function testHandleInvalidJson()
+ {
+ $this->response->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(self::$successfulCode);
+
+ $this->response->expects($this->once())
+ ->method('getBody')
+ ->willReturn('param');
+
+ $this->dataDecoder = $this->getMockBuilder(DecoderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->dataDecoder->expects($this->once())
+ ->method('decode')
+ ->with('param')
+ ->willThrowException(new \Exception(self::$errorMessage, 30));
+
+ $this->responseHandler = $this->objectManager->getObject(ResponseHandler::class, [
+ 'dataDecoder' => $this->dataDecoder
+ ]);
+
+ $this->responseHandler->handle($this->response);
+ }
+
+ public function testHandle()
+ {
+ $this->response->expects($this->any())
+ ->method('getStatus')
+ ->willReturn(self::$successfulCode);
+
+ $this->response->expects($this->once())
+ ->method('getBody')
+ ->willReturn(self::$testJson);
+
+ $this->dataDecoder->expects($this->once())
+ ->method('decode')
+ ->willReturn(json_decode(self::$testJson, 1));
+
+ $decodedResponseBody = $this->responseHandler->handle($this->response);
+ $this->assertEquals($decodedResponseBody, ['id' => 1]);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php
new file mode 100644
index 0000000000000..f7aa65f842b91
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php
@@ -0,0 +1,338 @@
+createCaseBuilder = $this->getMockBuilder(CreateCaseBuilderInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->apiClient = $this->getMockBuilder(ApiClient::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->gateway = new Gateway(
+ $this->createCaseBuilder,
+ $this->apiClient
+ );
+ }
+
+ public function testCreateCaseForSpecifiedOrder()
+ {
+ $dummyOrderId = 1;
+ $dummySignifydInvestigationId = 42;
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'investigationId' => $dummySignifydInvestigationId
+ ]);
+
+ $this->createCaseBuilder
+ ->expects($this->atLeastOnce())
+ ->method('build')
+ ->with($this->equalTo($dummyOrderId))
+ ->willReturn([]);
+
+ $result = $this->gateway->createCase($dummyOrderId);
+ $this->assertEquals(42, $result);
+ }
+
+ public function testCreateCaseCallsValidApiMethod()
+ {
+ $dummyOrderId = 1;
+ $dummySignifydInvestigationId = 42;
+ $this->createCaseBuilder
+ ->method('build')
+ ->willReturn([]);
+
+ $this->apiClient
+ ->expects($this->atLeastOnce())
+ ->method('makeApiCall')
+ ->with(
+ $this->equalTo('/cases'),
+ $this->equalTo('POST'),
+ $this->isType('array')
+ )
+ ->willReturn([
+ 'investigationId' => $dummySignifydInvestigationId
+ ]);
+
+ $result = $this->gateway->createCase($dummyOrderId);
+ $this->assertEquals(42, $result);
+ }
+
+ public function testCreateCaseNormalFlow()
+ {
+ $dummyOrderId = 1;
+ $dummySignifydInvestigationId = 42;
+ $this->createCaseBuilder
+ ->method('build')
+ ->willReturn([]);
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'investigationId' => $dummySignifydInvestigationId
+ ]);
+
+ $returnedInvestigationId = $this->gateway->createCase($dummyOrderId);
+ $this->assertEquals(
+ $dummySignifydInvestigationId,
+ $returnedInvestigationId,
+ 'Method must return value specified in "investigationId" response parameter'
+ );
+ }
+
+ public function testCreateCaseWithFailedApiCall()
+ {
+ $dummyOrderId = 1;
+ $apiCallFailureMessage = 'Api call failed';
+ $this->createCaseBuilder
+ ->method('build')
+ ->willReturn([]);
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willThrowException(new ApiCallException($apiCallFailureMessage));
+
+ $this->expectException(
+ GatewayException::class,
+ $apiCallFailureMessage
+ );
+ $this->gateway->createCase($dummyOrderId);
+ }
+
+ public function testCreateCaseWithMissedResponseRequiredData()
+ {
+ $dummyOrderId = 1;
+ $this->createCaseBuilder
+ ->method('build')
+ ->willReturn([]);
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'someOtherParameter' => 'foo',
+ ]);
+
+ $this->expectException(GatewayException::class);
+ $this->gateway->createCase($dummyOrderId);
+ }
+
+ public function testCreateCaseWithAdditionalResponseData()
+ {
+ $dummyOrderId = 1;
+ $dummySignifydInvestigationId = 42;
+ $this->createCaseBuilder
+ ->method('build')
+ ->willReturn([]);
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'investigationId' => $dummySignifydInvestigationId,
+ 'someOtherParameter' => 'foo',
+ ]);
+
+ $returnedInvestigationId = $this->gateway->createCase($dummyOrderId);
+ $this->assertEquals(
+ $dummySignifydInvestigationId,
+ $returnedInvestigationId,
+ 'Method must return value specified in "investigationId" response parameter and ignore any other parameters'
+ );
+ }
+
+ public function testSubmitCaseForGuaranteeCallsValidApiMethod()
+ {
+ $dummySygnifydCaseId = 42;
+ $dummyDisposition = 'APPROVED';
+
+ $this->apiClient
+ ->expects($this->atLeastOnce())
+ ->method('makeApiCall')
+ ->with(
+ $this->equalTo('/guarantees'),
+ $this->equalTo('POST'),
+ $this->equalTo([
+ 'caseId' => $dummySygnifydCaseId
+ ])
+ )->willReturn([
+ 'disposition' => $dummyDisposition
+ ]);
+
+ $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ $this->assertEquals('APPROVED', $result);
+ }
+
+ public function testSubmitCaseForGuaranteeWithFailedApiCall()
+ {
+ $dummySygnifydCaseId = 42;
+ $apiCallFailureMessage = 'Api call failed';
+
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willThrowException(new ApiCallException($apiCallFailureMessage));
+
+ $this->expectException(
+ GatewayException::class,
+ $apiCallFailureMessage
+ );
+ $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ $this->assertEquals('Api call failed', $result);
+ }
+
+ public function testSubmitCaseForGuaranteeReturnsDisposition()
+ {
+ $dummySygnifydCaseId = 42;
+ $dummyDisposition = 'APPROVED';
+ $dummyGuaranteeId = 123;
+ $dummyRereviewCount = 0;
+
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'guaranteeId' => $dummyGuaranteeId,
+ 'disposition' => $dummyDisposition,
+ 'rereviewCount' => $dummyRereviewCount,
+ ]);
+
+ $actualDisposition = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ $this->assertEquals(
+ $dummyDisposition,
+ $actualDisposition,
+ 'Method must return guarantee disposition retrieved in Signifyd API response as a result'
+ );
+ }
+
+ public function testSubmitCaseForGuaranteeWithMissedDisposition()
+ {
+ $dummySygnifydCaseId = 42;
+ $dummyGuaranteeId = 123;
+ $dummyRereviewCount = 0;
+
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'guaranteeId' => $dummyGuaranteeId,
+ 'rereviewCount' => $dummyRereviewCount,
+ ]);
+
+ $this->expectException(GatewayException::class);
+ $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ }
+
+ public function testSubmitCaseForGuaranteeWithUnexpectedDisposition()
+ {
+ $dummySygnifydCaseId = 42;
+ $dummyUnexpectedDisposition = 'UNEXPECTED';
+
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'disposition' => $dummyUnexpectedDisposition,
+ ]);
+
+ $this->expectException(GatewayException::class);
+ $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ $this->assertEquals('UNEXPECTED', $result);
+ }
+
+ /**
+ * @dataProvider supportedGuaranteeDispositionsProvider
+ */
+ public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpectedDisposition)
+ {
+ $dummySygnifydCaseId = 42;
+
+ $this->apiClient
+ ->method('makeApiCall')
+ ->willReturn([
+ 'disposition' => $dummyExpectedDisposition,
+ ]);
+
+ try {
+ $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
+ $this->assertEquals($dummyExpectedDisposition, $result);
+ } catch (GatewayException $e) {
+ $this->fail(sprintf(
+ 'Expected disposition "%s" was not accepted with message "%s"',
+ $dummyExpectedDisposition,
+ $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * Checks a test case when guarantee for a case is successfully canceled
+ *
+ * @covers \Magento\Signifyd\Model\SignifydGateway\Gateway::cancelGuarantee
+ */
+ public function testCancelGuarantee()
+ {
+ $caseId = 123;
+
+ $this->apiClient->expects(self::once())
+ ->method('makeApiCall')
+ ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED])
+ ->willReturn(['disposition' => Gateway::GUARANTEE_CANCELED]);
+
+ $result = $this->gateway->cancelGuarantee($caseId);
+ self::assertEquals(Gateway::GUARANTEE_CANCELED, $result);
+ }
+
+ /**
+ * Checks a case when API request returns unexpected guarantee disposition.
+ *
+ * @covers \Magento\Signifyd\Model\SignifydGateway\Gateway::cancelGuarantee
+ * @expectedException \Magento\Signifyd\Model\SignifydGateway\GatewayException
+ * @expectedExceptionMessage API returned unexpected disposition: DECLINED.
+ */
+ public function testCancelGuaranteeWithUnexpectedDisposition()
+ {
+ $caseId = 123;
+
+ $this->apiClient->expects(self::once())
+ ->method('makeApiCall')
+ ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED])
+ ->willReturn(['disposition' => Gateway::GUARANTEE_DECLINED]);
+
+ $result = $this->gateway->cancelGuarantee($caseId);
+ $this->assertEquals(Gateway::GUARANTEE_CANCELED, $result);
+ }
+
+ public function supportedGuaranteeDispositionsProvider()
+ {
+ return [
+ 'APPROVED' => ['APPROVED'],
+ 'DECLINED' => ['DECLINED'],
+ 'PENDING' => ['PENDING'],
+ 'CANCELED' => ['CANCELED'],
+ 'IN_REVIEW' => ['IN_REVIEW'],
+ 'UNREQUESTED' => ['UNREQUESTED'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookMessageReaderTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookMessageReaderTest.php
new file mode 100644
index 0000000000000..0dfdf4980fb58
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookMessageReaderTest.php
@@ -0,0 +1,114 @@
+decoder = $this->getMockBuilder(DecoderInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->webhookMessageFactory = $this->getMockBuilder(WebhookMessageFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->webhookRequest = $this->getMockBuilder(WebhookRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = new WebhookMessageReader(
+ $this->decoder,
+ $this->webhookMessageFactory
+ );
+ }
+
+ /**
+ * Tests successful reading webhook message from request.
+ *
+ */
+ public function testReadSuccess()
+ {
+ $rawBody = 'body';
+ $topic = 'topic';
+ $decodedData = ['status' => "DISMISSED", 'orderId' => '19418'];
+
+ $this->webhookRequest->expects($this->once())
+ ->method('getBody')
+ ->willReturn($rawBody);
+ $this->webhookRequest->expects($this->once())
+ ->method('getEventTopic')
+ ->willReturn('topic');
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->with($rawBody)
+ ->willReturn($decodedData);
+ $webhookMessage = $this->getMockBuilder(WebhookMessage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->webhookMessageFactory->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'data' => $decodedData,
+ 'eventTopic' => $topic
+ ]
+ )
+ ->willReturn($webhookMessage);
+
+ $this->assertEquals(
+ $webhookMessage,
+ $this->model->read($this->webhookRequest)
+ );
+ }
+
+ /**
+ * Tests reading failure webhook message from request.
+ *
+ * @expectedException \InvalidArgumentException
+ */
+ public function testReadFail()
+ {
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->willThrowException(new \Exception('Error'));
+
+ $this->model->read($this->webhookRequest);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookRequestValidatorTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookRequestValidatorTest.php
new file mode 100644
index 0000000000000..5ae6b95a77548
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Response/WebhookRequestValidatorTest.php
@@ -0,0 +1,231 @@
+config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->decoder = $this->getMockBuilder(DecoderInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->model = new WebhookRequestValidator(
+ $this->config,
+ $this->decoder
+ );
+ }
+
+ /**
+ * Tests successful cases.
+ *
+ * @param string $body
+ * @param string $topic
+ * @param string $hash
+ * @param int$callConfigCount
+ * @dataProvider validateSuccessDataProvider
+ */
+ public function testValidateSuccess($body, $topic, $hash, $callConfigCount)
+ {
+ $this->config->expects($this->exactly($callConfigCount))
+ ->method('getApiKey')
+ ->willReturn('GpFZZnxGgIxuI8BazSm3v6eGK');
+
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->with($body)
+ ->willReturn(['status' => "DISMISSED", 'orderId' => '19418']);
+
+ $webhookRequest = $this->createWebhookRequest($body, $topic, $hash);
+
+ $this->assertTrue(
+ $this->model->validate($webhookRequest)
+ );
+ }
+
+ /**
+ * @case 1. All data are correct, event topic has real value
+ * @case 2. All data are correct, event topic has test value
+ * @return array
+ */
+ public function validateSuccessDataProvider()
+ {
+ return [
+ 1 => [
+ 'body' => '{ status: "DISMISSED", orderId: "19418" }',
+ 'topic' => 'cases/creation',
+ 'hash' => 'KWR8Bzu3tinEpDviw1opWSMJGFqfpA79nNGp0TEYM6Q=',
+ 'callConfigCount' => 1
+ ],
+ 2 => [
+ 'body' => '{ status: "DISMISSED", orderId: "19418" }',
+ 'topic' => 'cases/test',
+ 'hash' => '6npAahliNbzYo/Qi4+g+JeqPhLFgg19sIbuxDLmvobw=',
+ 'callConfigCount' => 0
+ ]
+ ];
+ }
+
+ /**
+ * Case with wrong event topic
+ *
+ * @param string $topic
+ * @dataProvider validationTopicFailsDataProvider
+ */
+ public function testValidationTopicFails($topic)
+ {
+ $body = '{ status: "DISMISSED", orderId: "19418" }';
+ $hash = 'KWR8Bzu3tinEpDviw1opWSMJGFqfpA79nNGp0TEYM6Q=';
+
+ $this->config->expects($this->never())
+ ->method('getApiKey');
+
+ $this->decoder->expects($this->never())
+ ->method('decode');
+
+ $webhookRequest = $this->createWebhookRequest($body, $topic, $hash);
+
+ $this->assertFalse(
+ $this->model->validate($webhookRequest),
+ 'Negative webhook event topic value validation fails'
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function validationTopicFailsDataProvider()
+ {
+ return [
+ ['wrong topic' => 'bla-bla-topic'],
+ ['empty topic' => '']
+ ];
+ }
+
+ /**
+ * Case with wrong webhook request body
+ *
+ * @param string $body
+ * @dataProvider validationBodyFailsDataProvider
+ */
+ public function testValidationBodyFails($body)
+ {
+ $topic = 'cases/creation';
+ $hash = 'KWR8Bzu3tinEpDviw1opWSMJGFqfpA79nNGp0TEYM6Q=';
+ $webhookRequest = $this->createWebhookRequest($body, $topic, $hash);
+
+ $this->config->expects($this->never())
+ ->method('getApiKey');
+
+ if (empty($body)) {
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->with($body)
+ ->willReturn('');
+ } else {
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->with($body)
+ ->willThrowException(new \Exception('Error'));
+ }
+
+ $this->assertFalse(
+ $this->model->validate($webhookRequest),
+ 'Negative webhook request body validation fails'
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function validationBodyFailsDataProvider()
+ {
+ return [
+ ['Empty request body' => ''],
+ ['Bad request body' => '{ bad data}']
+ ];
+ }
+
+ /**
+ * Case with wrong hash
+ */
+ public function testValidationHashFails()
+ {
+ $topic = 'cases/creation';
+ $body = '{ status: "DISMISSED", orderId: "19418" }';
+ $hash = 'wrong hash';
+ $webhookRequest = $this->createWebhookRequest($body, $topic, $hash);
+
+ $this->config->expects($this->once())
+ ->method('getApiKey')
+ ->willReturn('GpFZZnxGgIxuI8BazSm3v6eGK');
+
+ $this->decoder->expects($this->once())
+ ->method('decode')
+ ->with($body)
+ ->willReturn(['status' => "DISMISSED", 'orderId' => '19418']);
+
+ $this->assertFalse(
+ $this->model->validate($webhookRequest),
+ 'Negative webhook hash validation fails'
+ );
+ }
+
+ /**
+ * Returns mocked WebhookRequest
+ *
+ * @param string $body
+ * @param string $topic
+ * @param string $hash
+ * @return WebhookRequest|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createWebhookRequest($body, $topic, $hash)
+ {
+ $webhookRequest = $this->getMockBuilder(WebhookRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookRequest->expects($this->once())
+ ->method('getBody')
+ ->willReturn($body);
+ $webhookRequest->expects($this->once())
+ ->method('getEventTopic')
+ ->willReturn($topic);
+ $webhookRequest->expects($this->once())
+ ->method('getHash')
+ ->willReturn($hash);
+
+ return $webhookRequest;
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydOrderSessionIdTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydOrderSessionIdTest.php
new file mode 100644
index 0000000000000..9d3061f240c21
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydOrderSessionIdTest.php
@@ -0,0 +1,60 @@
+identityGenerator = $this->getMockBuilder(IdentityGeneratorInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->signifydOrderSessionId = new SignifydOrderSessionId($this->identityGenerator);
+ }
+
+ /**
+ * Tests method by passing quoteId parameter
+ *
+ * @covers \Magento\Signifyd\Model\SignifydOrderSessionId::get
+ */
+ public function testGetByQuoteId()
+ {
+ $quoteId = 1;
+ $signifydOrderSessionId = 'asdfzxcv';
+
+ $this->identityGenerator->expects(self::once())
+ ->method('generateIdForData')
+ ->with($quoteId)
+ ->willReturn($signifydOrderSessionId);
+
+ $this->assertEquals(
+ $signifydOrderSessionId,
+ $this->signifydOrderSessionId->get($quoteId)
+ );
+ }
+}
diff --git a/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php
new file mode 100644
index 0000000000000..e2870953ec280
--- /dev/null
+++ b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php
@@ -0,0 +1,255 @@
+config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isActive'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->creationService = $this->getMockBuilder(CaseCreationServiceInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['createForOrder'])
+ ->getMock();
+
+ $this->observer = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getEvent'])
+ ->getMock();
+
+ $this->event = $this->getMockBuilder(Event::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData'])
+ ->getMock();
+
+ $this->placeOrder = new PlaceOrder(
+ $this->config,
+ $this->creationService,
+ $this->logger
+ );
+ }
+
+ /**
+ * Checks a test case when Signifyd module is disabled.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ */
+ public function testExecuteWithDisabledModule()
+ {
+ $this->withActiveSignifydIntegration(false);
+
+ $this->creationService->expects(self::never())
+ ->method('createForOrder');
+
+ $this->placeOrder->execute($this->observer);
+ }
+
+ /**
+ * Checks a test case when the observer event returns empty an order entity.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ */
+ public function testExecuteWithoutOrder()
+ {
+ $this->withActiveSignifydIntegration(true);
+ $this->withOrderEntity(null);
+
+ $this->creationService->expects(self::never())
+ ->method('createForOrder');
+
+ $this->placeOrder->execute($this->observer);
+ }
+
+ /**
+ * Checks a test case when the order placed with offline payment method.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ */
+ public function testExecuteWithOfflinePayment()
+ {
+ $orderId = 1;
+ $this->withActiveSignifydIntegration(true);
+ $this->withOrderEntity($orderId);
+ $this->withAvailablePaymentMethod(false);
+
+ $this->creationService->expects(self::never())
+ ->method('createForOrder');
+
+ $this->placeOrder->execute($this->observer);
+ }
+
+ /**
+ * Checks a test case when case creation service fails.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ */
+ public function testExecuteWithFailedCaseCreation()
+ {
+ $orderId = 1;
+ $exceptionMessage = __('Case with the same order id already exists.');
+
+ $this->withActiveSignifydIntegration(true);
+ $this->withOrderEntity($orderId);
+ $this->withAvailablePaymentMethod(true);
+
+ $this->creationService->method('createForOrder')
+ ->with(self::equalTo($orderId))
+ ->willThrowException(new AlreadyExistsException($exceptionMessage));
+
+ $this->logger->method('error')
+ ->with(self::equalTo($exceptionMessage));
+
+ $result = $this->placeOrder->execute($this->observer);
+ $this->assertNull($result);
+ }
+
+ /**
+ * Checks a test case when observer successfully calls case creation service.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ */
+ public function testExecute()
+ {
+ $orderId = 1;
+
+ $this->withActiveSignifydIntegration(true);
+ $this->withOrderEntity($orderId);
+ $this->withAvailablePaymentMethod(true);
+
+ $this->creationService
+ ->method('createForOrder')
+ ->with(self::equalTo($orderId));
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $this->placeOrder->execute($this->observer);
+ }
+
+ /**
+ * Specifies order entity mock execution.
+ *
+ * @param int $orderId
+ * @return void
+ */
+ private function withOrderEntity($orderId)
+ {
+ $this->orderEntity = $this->getMockBuilder(OrderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->orderEntity->method('getEntityId')
+ ->willReturn($orderId);
+
+ $this->observer->method('getEvent')
+ ->willReturn($this->event);
+
+ $this->event->method('getData')
+ ->with('order')
+ ->willReturn($this->orderEntity);
+ }
+
+ /**
+ * Specifies config mock execution.
+ *
+ * @param bool $isActive
+ * @return void
+ */
+ private function withActiveSignifydIntegration($isActive)
+ {
+ $this->config->method('isActive')
+ ->willReturn($isActive);
+ }
+
+ /**
+ * Specifies payment method mock execution.
+ *
+ * @param bool $isAvailable
+ * @return void
+ */
+ private function withAvailablePaymentMethod($isAvailable)
+ {
+ /** @var MethodInterface|MockObject $paymentMethod */
+ $paymentMethod = $this->getMockBuilder(MethodInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ /**
+ * The code depends on implementation but not interface
+ * because order payment implements two interfaces
+ */
+ /** @var Payment|MockObject $orderPayment */
+ $orderPayment = $this->getMockBuilder(Payment::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->orderEntity->method('getPayment')
+ ->willReturn($orderPayment);
+
+ $orderPayment->method('getMethodInstance')
+ ->willReturn($paymentMethod);
+
+ $paymentMethod->method('isOffline')
+ ->willReturn(!$isAvailable);
+ }
+}
diff --git a/app/code/Magento/Signifyd/Ui/Component/Listing/Column/Guarantee/Options.php b/app/code/Magento/Signifyd/Ui/Component/Listing/Column/Guarantee/Options.php
new file mode 100644
index 0000000000000..1e6234a8e27a9
--- /dev/null
+++ b/app/code/Magento/Signifyd/Ui/Component/Listing/Column/Guarantee/Options.php
@@ -0,0 +1,56 @@
+escaper = $escaper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toOptionArray()
+ {
+ return [
+ [
+ 'value' => CaseInterface::GUARANTEE_DECLINED,
+ 'label' => $this->escaper->escapeHtml(__('Declined'))
+ ],
+ [
+ 'value' => CaseInterface::GUARANTEE_APPROVED,
+ 'label' => $this->escaper->escapeHtml(__('Approved'))
+ ],
+ [
+ 'value' => CaseInterface::GUARANTEE_CANCELED,
+ 'label' => $this->escaper->escapeHtml(__('Canceled'))
+ ],
+ [
+ 'value' => CaseInterface::GUARANTEE_PENDING,
+ 'label' => $this->escaper->escapeHtml(__('Pending'))
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Signifyd/composer.json b/app/code/Magento/Signifyd/composer.json
new file mode 100644
index 0000000000000..59326a48f3e85
--- /dev/null
+++ b/app/code/Magento/Signifyd/composer.json
@@ -0,0 +1,35 @@
+{
+ "name": "magento/module-signifyd",
+ "description": "Submitting Case Entry to Signifyd on Order Creation",
+ "config": {
+ "sort-packages": true
+ },
+ "require": {
+ "magento/framework": "100.3.*",
+ "magento/module-backend": "100.3.*",
+ "magento/module-checkout": "100.3.*",
+ "magento/module-config": "100.3.*",
+ "magento/module-customer": "100.3.*",
+ "magento/module-directory": "100.3.*",
+ "magento/module-payment": "100.3.*",
+ "magento/module-sales": "100.3.*",
+ "magento/module-store": "100.3.*",
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0"
+ },
+ "suggest": {
+ "magento/module-config": "100.3.*"
+ },
+ "type": "magento2-module",
+ "version": "100.3.0-dev",
+ "license": [
+ "proprietary"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Signifyd\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Signifyd/etc/acl.xml b/app/code/Magento/Signifyd/etc/acl.xml
new file mode 100644
index 0000000000000..32f0493fbcad9
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/acl.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/adminhtml/di.xml b/app/code/Magento/Signifyd/etc/adminhtml/di.xml
new file mode 100644
index 0000000000000..c771d67216d43
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/adminhtml/di.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/adminhtml/routes.xml b/app/code/Magento/Signifyd/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..c078ab3c8c4c1
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/adminhtml/system.xml b/app/code/Magento/Signifyd/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..d9ba2f7ffdff2
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/adminhtml/system.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ sales
+ Magento_Sales::fraud_protection
+
+ signifyd-logo-header
+
+ Magento\Signifyd\Block\Adminhtml\System\Config\Fieldset\Info
+ signifyd-about-header
+
+ Benefits:
+ - Grow your business without fear of fraud
+ - Accept more orders and maximize your revenue
+ - Automate order review and shift fraud off your plate
]]>
+
+ https://www.signifyd.com/magento-guaranteed-fraud-protection
+
+
+ signifyd-about-header
+
+ View our setup guide for step-by-step instructions on how to integrate Signifyd with Magento.
For support contact support@signifyd.com.]]>
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ fraud_protection/signifyd/active
+
+
+
+ settings page in the Signifyd console]]>
+ fraud_protection/signifyd/api_key
+ Magento\Config\Model\Config\Backend\Encrypted
+
+
+
+ fraud_protection/signifyd/api_url
+ Don’t change unless asked to do so.
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ fraud_protection/signifyd/debug
+
+
+
+ configure a guarantee completed webhook in Signifyd. Webhooks are used to sync Signifyd`s guarantee decisions back to Magento.]]>
+ signifyd/webhooks/handler
+ Magento\Signifyd\Block\Adminhtml\System\Config\Field\WebhookUrl
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/config.xml b/app/code/Magento/Signifyd/etc/config.xml
new file mode 100644
index 0000000000000..804342a14bb08
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/config.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 0
+ https://api.signifyd.com/v2/
+
+ 0
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/db_schema.xml b/app/code/Magento/Signifyd/etc/db_schema.xml
new file mode 100644
index 0000000000000..731ae1e94b2bb
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/db_schema.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/db_schema_whitelist.json b/app/code/Magento/Signifyd/etc/db_schema_whitelist.json
new file mode 100644
index 0000000000000..31f753dbb3b39
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/db_schema_whitelist.json
@@ -0,0 +1,28 @@
+{
+ "signifyd_case": {
+ "column": {
+ "entity_id": true,
+ "order_id": true,
+ "case_id": true,
+ "guarantee_eligible": true,
+ "guarantee_disposition": true,
+ "status": true,
+ "score": true,
+ "associated_team": true,
+ "review_disposition": true,
+ "created_at": true,
+ "updated_at": true
+ },
+ "constraint": {
+ "PRIMARY": true,
+ "SIGNIFYD_CASE_ORDER_ID_SALES_ORDER_ENTITY_ID": true,
+ "SIGNIFYD_CASE_ORDER_ID": true,
+ "SIGNIFYD_CASE_CASE_ID": true
+ }
+ },
+ "sales_order_grid": {
+ "column": {
+ "signifyd_guarantee_status": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml
new file mode 100644
index 0000000000000..92ad8a0bfd87a
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/di.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ U
+
+
+
+
+
+
+ Magento\Payment\Gateway\Config\Config
+ SignifydAvsDefaultMapper
+ SignifydCvvDefaultMapper
+
+
+
+
+ Magento\Sales\Model\ResourceModel\Order\Grid
+
+
+
+
+
+ - Magento\Signifyd\Model\SalesOrderGrid\NotSyncedOrderIdListProvider
+
+
+
+
+
+ SignifydIdListProvider
+
+ -
+
- signifyd_case
+ - entity_id
+ - order_id
+
+
+
+ - signifyd_case.guarantee_disposition
+
+
+
+
+
+ sales
+
+
+
+
+ Magento_Signifyd
+ signifyd_payment_mapping.xsd
+
+
+
+
+ Magento\Signifyd\Model\PaymentMethodMapper\XmlToArrayConfigConverter
+ PaymentMapperSchemaLocator
+ signifyd_payment_mapping.xml
+
+
+
+
+ PaymentMapperConfigReader
+ signifyd_payment_list_cache
+
+
+
+
+ PaymentMethodConfigData
+
+
+
+
+
+ - 1
+ - 1
+
+
+ - 1
+ - 1
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/events.xml b/app/code/Magento/Signifyd/etc/events.xml
new file mode 100644
index 0000000000000..a89b56ddf13c6
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/events.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/frontend/di.xml b/app/code/Magento/Signifyd/etc/frontend/di.xml
new file mode 100644
index 0000000000000..08a690d1a9930
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/frontend/di.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/frontend/routes.xml b/app/code/Magento/Signifyd/etc/frontend/routes.xml
new file mode 100644
index 0000000000000..5803f59d8624b
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/frontend/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/module.xml b/app/code/Magento/Signifyd/etc/module.xml
new file mode 100644
index 0000000000000..d5adcba88ad9a
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/module.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml
new file mode 100644
index 0000000000000..096a968167173
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+ braintree
+ PAYMENT_CARD
+
+
+ braintree_paypal
+ PAYPAL_ACCOUNT
+
+
+ paypal_express
+ PAYPAL_ACCOUNT
+
+
+ paypal_express_bml
+ PAYPAL_ACCOUNT
+
+
+ payflow_express
+ PAYPAL_ACCOUNT
+
+
+ payflow_express_bml
+ PAYPAL_ACCOUNT
+
+
+ payflowpro
+ PAYMENT_CARD
+
+
+ payflow_link
+ PAYMENT_CARD
+
+
+ payflow_advanced
+ PAYMENT_CARD
+
+
+ hosted_pro
+ PAYMENT_CARD
+
+
+ authorizenet_directpost
+ PAYMENT_CARD
+
+
+ worldpay
+ PAYMENT_CARD
+
+
+ eway
+ PAYMENT_CARD
+
+
+ cybersource
+ PAYMENT_CARD
+
+
+ free
+ FREE
+
+
+
diff --git a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd
new file mode 100644
index 0000000000000..f48805c328e09
--- /dev/null
+++ b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/i18n/en_US.csv b/app/code/Magento/Signifyd/i18n/en_US.csv
new file mode 100644
index 0000000000000..40772188dac9e
--- /dev/null
+++ b/app/code/Magento/Signifyd/i18n/en_US.csv
@@ -0,0 +1,46 @@
+OPEN,Open
+PROCESSING,Processing
+FLAGGED,Flagged
+DISMISSED,Dismissed
+HELD,Held
+GOOD,Good
+FRAUDULENT,Fraudulent
+UNSET,Unset
+NULL,Unset
+APPROVED,Approved
+DECLINED,Declined
+PENDING,Pending
+CANCELED,Canceled
+IN_REVIEW,"In review"
+Approved,Approved
+Declined,Declined
+Pending,Pending
+Canceled,Canceled
+"In Review","In Review"
+Unrequested,Unrequested
+"Learn more","Learn more"
+"This order already has associated case entity","This order already has associated case entity"
+"The case entity should not be empty.","The case entity should not be empty."
+"Cannot update Case entity.","Cannot update Case entity."
+"The ""%1"" should not be empty.","The ""%1"" should not be empty."
+"Case entity not found.","Case entity not found."
+"Case Update: New score for the order is %1. Previous score was %2.","Case Update: New score for the order is %1. Previous score was %2."
+"Awaiting the Signifyd guarantee disposition.","Awaiting the Signifyd guarantee disposition."
+"Only single entrance of ""%1"" node is required.","Only single entrance of ""%1"" node is required."
+"Not empty value for ""%1"" node is required.","Not empty value for ""%1"" node is required."
+"%1 must implement %2","%1 must implement %2"
+Error,Error
+"Cannot generate message.","Cannot generate message."
+"Message is generated.","Message is generated."
+"Case with the same order id already exists.","Case with the same order id already exists."
+"Fraud Protection Information","Fraud Protection Information"
+"Signifyd Guarantee Decision","Signifyd Guarantee Decision"
+"Fraud Protection Section","Fraud Protection Section"
+"Fraud Protection","Fraud Protection"
+"Protect your store from fraud with Guaranteed Fraud Protection by Signifyd.","Protect your store from fraud with Guaranteed Fraud Protection by Signifyd."
+Configuration,Configuration
+"Enable this Solution","Enable this Solution"
+"API Key","API Key"
+"API URL","API URL"
+Debug,Debug
+"Webhook URL","Webhook URL"
diff --git a/app/code/Magento/Signifyd/registration.php b/app/code/Magento/Signifyd/registration.php
new file mode 100644
index 0000000000000..72b11f7eac214
--- /dev/null
+++ b/app/code/Magento/Signifyd/registration.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml b/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml
new file mode 100644
index 0000000000000..a42b7f93b5b92
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml
@@ -0,0 +1,34 @@
+
+
+isEmptyCase()) {
+ return '';
+ }
+?>
+
+
+ = $block->escapeHtml(__('Fraud Protection Information')) ?>
+
+
+
+
+
+
+
+ = $block->escapeHtml(__('Signifyd Guarantee Decision')) ?> |
+ = $block->escapeHtml($block->getCaseGuaranteeDisposition()) ?> |
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Signifyd/view/adminhtml/ui_component/sales_order_grid.xml
new file mode 100644
index 0000000000000..91053d617f31f
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/adminhtml/ui_component/sales_order_grid.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ select
+
+ true
+ select
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/view/adminhtml/web/images/logo.png b/app/code/Magento/Signifyd/view/adminhtml/web/images/logo.png
new file mode 100644
index 0000000000000..7c6645e7c6c93
Binary files /dev/null and b/app/code/Magento/Signifyd/view/adminhtml/web/images/logo.png differ
diff --git a/app/code/Magento/Signifyd/view/adminhtml/web/js/request-send.js b/app/code/Magento/Signifyd/view/adminhtml/web/js/request-send.js
new file mode 100644
index 0000000000000..f55170336ca03
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/adminhtml/web/js/request-send.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'mageUtils',
+ 'Magento_Ui/js/form/components/button'
+], function (utils, Button) {
+ 'use strict';
+
+ return Button.extend({
+
+ /**
+ * Creates and submits form for Guarantee create/cancel
+ */
+ sendRequest: function () {
+ utils.submit({
+ url: this.requestURL,
+ data: this.data
+ });
+ }
+ });
+});
diff --git a/app/code/Magento/Signifyd/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Signifyd/view/frontend/layout/checkout_cart_index.xml
new file mode 100644
index 0000000000000..30472ae5e9ae9
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/frontend/layout/checkout_cart_index.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Signifyd/view/frontend/layout/checkout_index_index.xml
new file mode 100644
index 0000000000000..30472ae5e9ae9
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/frontend/layout/checkout_index_index.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml b/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml
new file mode 100644
index 0000000000000..356bab9c62ded
--- /dev/null
+++ b/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml
@@ -0,0 +1,18 @@
+
+isModuleActive()): ?>
+
+
diff --git a/composer.json b/composer.json
index be08cda437763..b383f5748ced3 100644
--- a/composer.json
+++ b/composer.json
@@ -193,6 +193,7 @@
"magento/module-security": "100.3.0-dev",
"magento/module-send-friend": "100.3.0-dev",
"magento/module-shipping": "100.3.0-dev",
+ "magento/module-signifyd": "100.3.0-dev",
"magento/module-sitemap": "100.3.0-dev",
"magento/module-store": "100.3.0-dev",
"magento/module-swagger": "100.3.0-dev",
diff --git a/composer.lock b/composer.lock
index 4ff91654a7796..4f7cbcb7e7e49 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +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"
],
- "content-hash": "782aacf0cff1ca2f4a5d7633071a9de2",
+ "hash": "569047f63be69ed4c05844cce0a39632",
+ "content-hash": "e05aaf7bf828fd2dd97fa1fb3cb71df3",
"packages": [
{
"name": "braintree/braintree_php",
@@ -51,7 +52,7 @@
}
],
"description": "Braintree PHP Client Library",
- "time": "2017-02-16T19:59:04+00:00"
+ "time": "2017-02-16 19:59:04"
},
{
"name": "colinmollenhour/cache-backend-file",
@@ -87,7 +88,7 @@
],
"description": "The stock Zend_Cache_Backend_File backend has extremely poor performance for cleaning by tags making it become unusable as the number of cached items increases. This backend makes many changes resulting in a huge performance boost, especially for tag cleaning.",
"homepage": "https://github.com/colinmollenhour/Cm_Cache_Backend_File",
- "time": "2016-05-02T16:24:47+00:00"
+ "time": "2016-05-02 16:24:47"
},
{
"name": "colinmollenhour/cache-backend-redis",
@@ -123,7 +124,7 @@
],
"description": "Zend_Cache backend using Redis with full support for tags.",
"homepage": "https://github.com/colinmollenhour/Cm_Cache_Backend_Redis",
- "time": "2017-03-25T04:54:24+00:00"
+ "time": "2017-03-25 04:54:24"
},
{
"name": "colinmollenhour/credis",
@@ -162,7 +163,7 @@
],
"description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.",
"homepage": "https://github.com/colinmollenhour/credis",
- "time": "2015-11-28T01:20:04+00:00"
+ "time": "2015-11-28 01:20:04"
},
{
"name": "colinmollenhour/php-redis-session-abstract",
@@ -199,7 +200,7 @@
],
"description": "A Redis-based session handler with optimistic locking",
"homepage": "https://github.com/colinmollenhour/php-redis-session-abstract",
- "time": "2017-04-19T14:21:43+00:00"
+ "time": "2017-04-19 14:21:43"
},
{
"name": "composer/ca-bundle",
@@ -255,7 +256,7 @@
"ssl",
"tls"
],
- "time": "2017-11-29T09:37:33+00:00"
+ "time": "2017-11-29 09:37:33"
},
{
"name": "composer/composer",
@@ -332,7 +333,7 @@
"dependency",
"package"
],
- "time": "2017-03-10T08:29:45+00:00"
+ "time": "2017-03-10 08:29:45"
},
{
"name": "composer/semver",
@@ -394,20 +395,20 @@
"validation",
"versioning"
],
- "time": "2016-08-30T16:08:34+00:00"
+ "time": "2016-08-30 16:08:34"
},
{
"name": "composer/spdx-licenses",
- "version": "1.2.0",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
- "reference": "2d899e9b33023c631854f36c39ef9f8317a7ab33"
+ "reference": "7e111c50db92fa2ced140f5ba23b4e261bc77a30"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/2d899e9b33023c631854f36c39ef9f8317a7ab33",
- "reference": "2d899e9b33023c631854f36c39ef9f8317a7ab33",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7e111c50db92fa2ced140f5ba23b4e261bc77a30",
+ "reference": "7e111c50db92fa2ced140f5ba23b4e261bc77a30",
"shasum": ""
},
"require": {
@@ -455,7 +456,7 @@
"spdx",
"validator"
],
- "time": "2018-01-03T16:37:06+00:00"
+ "time": "2018-01-31 13:17:27"
},
{
"name": "container-interop/container-interop",
@@ -486,7 +487,7 @@
],
"description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
"homepage": "https://github.com/container-interop/container-interop",
- "time": "2017-02-14T19:40:03+00:00"
+ "time": "2017-02-14 19:40:03"
},
{
"name": "justinrainbow/json-schema",
@@ -552,7 +553,7 @@
"json",
"schema"
],
- "time": "2017-10-21T13:15:38+00:00"
+ "time": "2017-10-21 13:15:38"
},
{
"name": "league/climate",
@@ -601,7 +602,7 @@
"php",
"terminal"
],
- "time": "2015-01-18T14:31:58+00:00"
+ "time": "2015-01-18 14:31:58"
},
{
"name": "magento/composer",
@@ -637,7 +638,7 @@
"AFL-3.0"
],
"description": "Magento composer library helps to instantiate Composer application and run composer commands.",
- "time": "2017-04-24T09:57:02+00:00"
+ "time": "2017-04-24 09:57:02"
},
{
"name": "magento/magento-composer-installer",
@@ -716,7 +717,7 @@
"composer-installer",
"magento"
],
- "time": "2017-12-29T16:45:24+00:00"
+ "time": "2017-12-29 16:45:24"
},
{
"name": "magento/zendframework1",
@@ -763,7 +764,7 @@
"ZF1",
"framework"
],
- "time": "2017-06-21T14:56:23+00:00"
+ "time": "2017-06-21 14:56:23"
},
{
"name": "monolog/monolog",
@@ -841,7 +842,7 @@
"logging",
"psr-3"
],
- "time": "2017-06-19T01:22:40+00:00"
+ "time": "2017-06-19 01:22:40"
},
{
"name": "oyejorge/less.php",
@@ -903,7 +904,7 @@
"php",
"stylesheet"
],
- "time": "2017-03-28T22:19:25+00:00"
+ "time": "2017-03-28 22:19:25"
},
{
"name": "paragonie/random_compat",
@@ -951,7 +952,7 @@
"pseudorandom",
"random"
],
- "time": "2017-09-27T21:40:39+00:00"
+ "time": "2017-09-27 21:40:39"
},
{
"name": "pelago/emogrifier",
@@ -1011,7 +1012,7 @@
],
"description": "Converts CSS styles into inline style attributes in your HTML code",
"homepage": "http://www.pelagodesign.com/sidecar/emogrifier/",
- "time": "2017-03-02T12:51:48+00:00"
+ "time": "2017-03-02 12:51:48"
},
{
"name": "phpseclib/phpseclib",
@@ -1103,7 +1104,7 @@
"x.509",
"x509"
],
- "time": "2017-11-29T06:38:08+00:00"
+ "time": "2017-11-29 06:38:08"
},
{
"name": "psr/container",
@@ -1152,7 +1153,7 @@
"container-interop",
"psr"
],
- "time": "2017-02-14T16:28:37+00:00"
+ "time": "2017-02-14 16:28:37"
},
{
"name": "psr/log",
@@ -1199,7 +1200,7 @@
"psr",
"psr-3"
],
- "time": "2016-10-10T12:19:37+00:00"
+ "time": "2016-10-10 12:19:37"
},
{
"name": "ramsey/uuid",
@@ -1281,7 +1282,7 @@
"identifier",
"uuid"
],
- "time": "2017-03-26T20:37:53+00:00"
+ "time": "2017-03-26 20:37:53"
},
{
"name": "seld/cli-prompt",
@@ -1329,20 +1330,20 @@
"input",
"prompt"
],
- "time": "2017-03-18T11:32:45+00:00"
+ "time": "2017-03-18 11:32:45"
},
{
"name": "seld/jsonlint",
- "version": "1.7.0",
+ "version": "1.7.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/jsonlint.git",
- "reference": "9b355654ea99460397b89c132b5c1087b6bf4473"
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9b355654ea99460397b89c132b5c1087b6bf4473",
- "reference": "9b355654ea99460397b89c132b5c1087b6bf4473",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38",
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38",
"shasum": ""
},
"require": {
@@ -1378,7 +1379,7 @@
"parser",
"validator"
],
- "time": "2018-01-03T12:13:57+00:00"
+ "time": "2018-01-24 12:46:19"
},
{
"name": "seld/phar-utils",
@@ -1422,7 +1423,7 @@
"keywords": [
"phra"
],
- "time": "2015-10-13T18:44:15+00:00"
+ "time": "2015-10-13 18:44:15"
},
{
"name": "sjparkinson/static-review",
@@ -1476,20 +1477,20 @@
],
"description": "An extendable framework for version control hooks.",
"abandoned": "phpro/grumphp",
- "time": "2014-09-22T08:40:36+00:00"
+ "time": "2014-09-22 08:40:36"
},
{
"name": "symfony/console",
- "version": "v2.8.33",
+ "version": "v2.8.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "a4bd0f02ea156cf7b5138774a7ba0ab44d8da4fe"
+ "reference": "162ca7d0ea597599967aa63b23418e747da0896b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/a4bd0f02ea156cf7b5138774a7ba0ab44d8da4fe",
- "reference": "a4bd0f02ea156cf7b5138774a7ba0ab44d8da4fe",
+ "url": "https://api.github.com/repos/symfony/console/zipball/162ca7d0ea597599967aa63b23418e747da0896b",
+ "reference": "162ca7d0ea597599967aa63b23418e747da0896b",
"shasum": ""
},
"require": {
@@ -1537,7 +1538,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:36:31+00:00"
+ "time": "2018-01-29 08:54:45"
},
{
"name": "symfony/debug",
@@ -1594,11 +1595,11 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
- "time": "2016-07-30T07:22:48+00:00"
+ "time": "2016-07-30 07:22:48"
},
{
"name": "symfony/event-dispatcher",
- "version": "v2.8.33",
+ "version": "v2.8.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
@@ -1654,11 +1655,11 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:36:31+00:00"
+ "time": "2018-01-03 07:36:31"
},
{
"name": "symfony/filesystem",
- "version": "v3.4.3",
+ "version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
@@ -1703,11 +1704,11 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:37:34+00:00"
+ "time": "2018-01-03 07:37:34"
},
{
"name": "symfony/finder",
- "version": "v3.4.3",
+ "version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@@ -1752,20 +1753,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:37:34+00:00"
+ "time": "2018-01-03 07:37:34"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
+ "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
- "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+ "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"shasum": ""
},
"require": {
@@ -1777,7 +1778,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -1811,20 +1812,20 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-30 19:27:44"
},
{
"name": "symfony/process",
- "version": "v2.8.33",
+ "version": "v2.8.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "ea3226daa3c6789efa39570bfc6e5d55f7561a0a"
+ "reference": "905efe90024caa75a2fc93f54e14b26f2a099d96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/ea3226daa3c6789efa39570bfc6e5d55f7561a0a",
- "reference": "ea3226daa3c6789efa39570bfc6e5d55f7561a0a",
+ "url": "https://api.github.com/repos/symfony/process/zipball/905efe90024caa75a2fc93f54e14b26f2a099d96",
+ "reference": "905efe90024caa75a2fc93f54e14b26f2a099d96",
"shasum": ""
},
"require": {
@@ -1860,7 +1861,7 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:36:31+00:00"
+ "time": "2018-01-29 08:54:45"
},
{
"name": "tedivm/jshrink",
@@ -1906,7 +1907,7 @@
"javascript",
"minifier"
],
- "time": "2015-07-04T07:35:09+00:00"
+ "time": "2015-07-04 07:35:09"
},
{
"name": "tubalmartin/cssmin",
@@ -1959,7 +1960,7 @@
"minify",
"yui"
],
- "time": "2017-05-16T13:45:26+00:00"
+ "time": "2017-05-16 13:45:26"
},
{
"name": "webonyx/graphql-php",
@@ -2006,7 +2007,7 @@
"api",
"graphql"
],
- "time": "2017-12-12T09:03:21+00:00"
+ "time": "2017-12-12 09:03:21"
},
{
"name": "zendframework/zend-captcha",
@@ -2063,7 +2064,7 @@
"captcha",
"zf2"
],
- "time": "2017-02-23T08:09:44+00:00"
+ "time": "2017-02-23 08:09:44"
},
{
"name": "zendframework/zend-code",
@@ -2116,7 +2117,7 @@
"code",
"zf2"
],
- "time": "2016-10-24T13:23:32+00:00"
+ "time": "2016-10-24 13:23:32"
},
{
"name": "zendframework/zend-config",
@@ -2172,32 +2173,32 @@
"config",
"zf2"
],
- "time": "2016-02-04T23:01:10+00:00"
+ "time": "2016-02-04 23:01:10"
},
{
"name": "zendframework/zend-console",
- "version": "2.6.0",
+ "version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-console.git",
- "reference": "cbbdfdfa0564aa20d1c6c6ef3daeafe6aec02360"
+ "reference": "e8aa08da83de3d265256c40ba45cd649115f0e18"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-console/zipball/cbbdfdfa0564aa20d1c6c6ef3daeafe6aec02360",
- "reference": "cbbdfdfa0564aa20d1c6c6ef3daeafe6aec02360",
+ "url": "https://api.github.com/repos/zendframework/zend-console/zipball/e8aa08da83de3d265256c40ba45cd649115f0e18",
+ "reference": "e8aa08da83de3d265256c40ba45cd649115f0e18",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
- "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-stdlib": "^2.7.7 || ^3.1"
},
"require-dev": {
- "fabpot/php-cs-fixer": "1.7.*",
- "phpunit/phpunit": "^4.0",
- "zendframework/zend-filter": "^2.6",
- "zendframework/zend-json": "^2.6",
- "zendframework/zend-validator": "^2.5"
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-filter": "^2.7.2",
+ "zendframework/zend-json": "^2.6 || ^3.0",
+ "zendframework/zend-validator": "^2.10.1"
},
"suggest": {
"zendframework/zend-filter": "To support DefaultRouteMatcher usage",
@@ -2206,8 +2207,8 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.6-dev",
- "dev-develop": "2.7-dev"
+ "dev-master": "2.7.x-dev",
+ "dev-develop": "2.8.x-dev"
}
},
"autoload": {
@@ -2219,12 +2220,13 @@
"license": [
"BSD-3-Clause"
],
- "homepage": "https://github.com/zendframework/zend-console",
+ "description": "Build console applications using getopt syntax or routing, complete with prompts",
"keywords": [
+ "ZendFramework",
"console",
- "zf2"
+ "zf"
],
- "time": "2016-02-09T17:15:12+00:00"
+ "time": "2018-01-25 19:08:04"
},
{
"name": "zendframework/zend-crypt",
@@ -2274,7 +2276,7 @@
"crypt",
"zf2"
],
- "time": "2016-02-03T23:46:30+00:00"
+ "time": "2016-02-03 23:46:30"
},
{
"name": "zendframework/zend-db",
@@ -2332,7 +2334,7 @@
"db",
"zf"
],
- "time": "2017-12-11T14:57:52+00:00"
+ "time": "2017-12-11 14:57:52"
},
{
"name": "zendframework/zend-di",
@@ -2379,7 +2381,7 @@
"di",
"zf2"
],
- "time": "2016-04-25T20:58:11+00:00"
+ "time": "2016-04-25 20:58:11"
},
{
"name": "zendframework/zend-escaper",
@@ -2423,7 +2425,7 @@
"escaper",
"zf2"
],
- "time": "2016-06-30T19:48:38+00:00"
+ "time": "2016-06-30 19:48:38"
},
{
"name": "zendframework/zend-eventmanager",
@@ -2470,7 +2472,7 @@
"eventmanager",
"zf2"
],
- "time": "2017-12-12T17:48:56+00:00"
+ "time": "2017-12-12 17:48:56"
},
{
"name": "zendframework/zend-filter",
@@ -2530,7 +2532,7 @@
"filter",
"zf2"
],
- "time": "2017-05-17T20:56:17+00:00"
+ "time": "2017-05-17 20:56:17"
},
{
"name": "zendframework/zend-form",
@@ -2608,7 +2610,7 @@
"form",
"zf"
],
- "time": "2017-12-06T21:09:08+00:00"
+ "time": "2017-12-06 21:09:08"
},
{
"name": "zendframework/zend-http",
@@ -2661,7 +2663,7 @@
"zend",
"zf"
],
- "time": "2017-10-13T12:06:24+00:00"
+ "time": "2017-10-13 12:06:24"
},
{
"name": "zendframework/zend-hydrator",
@@ -2719,7 +2721,7 @@
"hydrator",
"zf2"
],
- "time": "2016-02-18T22:38:26+00:00"
+ "time": "2016-02-18 22:38:26"
},
{
"name": "zendframework/zend-i18n",
@@ -2786,41 +2788,38 @@
"i18n",
"zf2"
],
- "time": "2017-05-17T17:00:12+00:00"
+ "time": "2017-05-17 17:00:12"
},
{
"name": "zendframework/zend-inputfilter",
- "version": "2.8.0",
+ "version": "2.8.1",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-inputfilter.git",
- "reference": "e7edd625f2fcdd72a719a7023114c5f4b4f38488"
+ "reference": "55d1430db559e9781b147e73c2c0ce6635d8efe2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/e7edd625f2fcdd72a719a7023114c5f4b4f38488",
- "reference": "e7edd625f2fcdd72a719a7023114c5f4b4f38488",
+ "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/55d1430db559e9781b147e73c2c0ce6635d8efe2",
+ "reference": "55d1430db559e9781b147e73c2c0ce6635d8efe2",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-filter": "^2.6",
+ "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1",
"zendframework/zend-stdlib": "^2.7 || ^3.0",
"zendframework/zend-validator": "^2.10.1"
},
"require-dev": {
"phpunit/phpunit": "^5.7.23 || ^6.4.3",
- "zendframework/zend-coding-standard": "~1.0.0",
- "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3"
- },
- "suggest": {
- "zendframework/zend-servicemanager": "To support plugin manager support"
+ "zendframework/zend-coding-standard": "~1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.8-dev",
- "dev-develop": "2.9-dev"
+ "dev-master": "2.8.x-dev",
+ "dev-develop": "2.9.x-dev"
},
"zf": {
"component": "Zend\\InputFilter",
@@ -2842,7 +2841,7 @@
"inputfilter",
"zf"
],
- "time": "2017-12-04T21:24:25+00:00"
+ "time": "2018-01-22 19:41:18"
},
{
"name": "zendframework/zend-json",
@@ -2897,7 +2896,7 @@
"json",
"zf2"
],
- "time": "2016-02-04T21:20:26+00:00"
+ "time": "2016-02-04 21:20:26"
},
{
"name": "zendframework/zend-loader",
@@ -2941,7 +2940,7 @@
"loader",
"zf2"
],
- "time": "2015-06-03T14:05:47+00:00"
+ "time": "2015-06-03 14:05:47"
},
{
"name": "zendframework/zend-log",
@@ -3012,7 +3011,7 @@
"logging",
"zf2"
],
- "time": "2017-05-17T16:03:26+00:00"
+ "time": "2017-05-17 16:03:26"
},
{
"name": "zendframework/zend-mail",
@@ -3074,7 +3073,7 @@
"mail",
"zf2"
],
- "time": "2017-06-08T20:03:58+00:00"
+ "time": "2017-06-08 20:03:58"
},
{
"name": "zendframework/zend-math",
@@ -3124,7 +3123,7 @@
"math",
"zf2"
],
- "time": "2016-04-07T16:29:53+00:00"
+ "time": "2016-04-07 16:29:53"
},
{
"name": "zendframework/zend-mime",
@@ -3175,7 +3174,7 @@
"mime",
"zf"
],
- "time": "2017-11-28T15:02:22+00:00"
+ "time": "2017-11-28 15:02:22"
},
{
"name": "zendframework/zend-modulemanager",
@@ -3235,7 +3234,7 @@
"modulemanager",
"zf"
],
- "time": "2017-12-02T06:11:18+00:00"
+ "time": "2017-12-02 06:11:18"
},
{
"name": "zendframework/zend-mvc",
@@ -3322,7 +3321,7 @@
"mvc",
"zf2"
],
- "time": "2016-02-23T15:24:59+00:00"
+ "time": "2016-02-23 15:24:59"
},
{
"name": "zendframework/zend-serializer",
@@ -3380,7 +3379,7 @@
"serializer",
"zf2"
],
- "time": "2017-11-20T22:21:04+00:00"
+ "time": "2017-11-20 22:21:04"
},
{
"name": "zendframework/zend-server",
@@ -3426,7 +3425,7 @@
"server",
"zf2"
],
- "time": "2016-06-20T22:27:55+00:00"
+ "time": "2016-06-20 22:27:55"
},
{
"name": "zendframework/zend-servicemanager",
@@ -3478,20 +3477,20 @@
"servicemanager",
"zf2"
],
- "time": "2017-12-05T16:27:36+00:00"
+ "time": "2017-12-05 16:27:36"
},
{
"name": "zendframework/zend-session",
- "version": "2.8.3",
+ "version": "2.8.4",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-session.git",
- "reference": "c14be63df39b0caee784e53cd57c43eb48efefea"
+ "reference": "9338f1ae483bcc18cc3b6c0347c8ba4f448b3e2a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-session/zipball/c14be63df39b0caee784e53cd57c43eb48efefea",
- "reference": "c14be63df39b0caee784e53cd57c43eb48efefea",
+ "url": "https://api.github.com/repos/zendframework/zend-session/zipball/9338f1ae483bcc18cc3b6c0347c8ba4f448b3e2a",
+ "reference": "9338f1ae483bcc18cc3b6c0347c8ba4f448b3e2a",
"shasum": ""
},
"require": {
@@ -3548,31 +3547,32 @@
"session",
"zf"
],
- "time": "2017-12-01T17:35:04+00:00"
+ "time": "2018-01-31 17:38:47"
},
{
"name": "zendframework/zend-soap",
- "version": "2.6.0",
+ "version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-soap.git",
- "reference": "2d6012e7231cce550219eccfc80836a028d20bf1"
+ "reference": "af03c32f0db2b899b3df8cfe29aeb2b49857d284"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-soap/zipball/2d6012e7231cce550219eccfc80836a028d20bf1",
- "reference": "2d6012e7231cce550219eccfc80836a028d20bf1",
+ "url": "https://api.github.com/repos/zendframework/zend-soap/zipball/af03c32f0db2b899b3df8cfe29aeb2b49857d284",
+ "reference": "af03c32f0db2b899b3df8cfe29aeb2b49857d284",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
+ "ext-soap": "*",
+ "php": "^5.6 || ^7.0",
"zendframework/zend-server": "^2.6.1",
"zendframework/zend-stdlib": "^2.7 || ^3.0",
"zendframework/zend-uri": "^2.5.2"
},
"require-dev": {
- "phpunit/phpunit": "^4.8",
- "squizlabs/php_codesniffer": "^2.3.1",
+ "phpunit/phpunit": "^5.7.21 || ^6.3",
+ "zendframework/zend-coding-standard": "~1.0.0",
"zendframework/zend-config": "^2.6",
"zendframework/zend-http": "^2.5.4"
},
@@ -3582,8 +3582,8 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.6-dev",
- "dev-develop": "2.7-dev"
+ "dev-master": "2.7.x-dev",
+ "dev-develop": "2.8.x-dev"
}
},
"autoload": {
@@ -3600,7 +3600,7 @@
"soap",
"zf2"
],
- "time": "2016-04-21T16:06:27+00:00"
+ "time": "2018-01-29 17:51:26"
},
{
"name": "zendframework/zend-stdlib",
@@ -3659,7 +3659,7 @@
"stdlib",
"zf2"
],
- "time": "2016-04-12T21:17:31+00:00"
+ "time": "2016-04-12 21:17:31"
},
{
"name": "zendframework/zend-text",
@@ -3706,7 +3706,7 @@
"text",
"zf2"
],
- "time": "2016-02-08T19:03:52+00:00"
+ "time": "2016-02-08 19:03:52"
},
{
"name": "zendframework/zend-uri",
@@ -3753,20 +3753,20 @@
"uri",
"zf2"
],
- "time": "2016-02-17T22:38:51+00:00"
+ "time": "2016-02-17 22:38:51"
},
{
"name": "zendframework/zend-validator",
- "version": "2.10.1",
+ "version": "2.10.2",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-validator.git",
- "reference": "010084ddbd33299bf51ea6f0e07f8f4e8bd832a8"
+ "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/010084ddbd33299bf51ea6f0e07f8f4e8bd832a8",
- "reference": "010084ddbd33299bf51ea6f0e07f8f4e8bd832a8",
+ "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9",
+ "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9",
"shasum": ""
},
"require": {
@@ -3801,8 +3801,8 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.10-dev",
- "dev-develop": "2.11-dev"
+ "dev-master": "2.10.x-dev",
+ "dev-develop": "2.11.x-dev"
},
"zf": {
"component": "Zend\\Validator",
@@ -3824,7 +3824,7 @@
"validator",
"zf2"
],
- "time": "2017-08-22T14:19:23+00:00"
+ "time": "2018-02-01 17:05:33"
},
{
"name": "zendframework/zend-view",
@@ -3911,7 +3911,7 @@
"view",
"zf2"
],
- "time": "2018-01-17T22:21:50+00:00"
+ "time": "2018-01-17 22:21:50"
}
],
"packages-dev": [
@@ -3967,7 +3967,7 @@
"constructor",
"instantiate"
],
- "time": "2015-06-14T21:17:01+00:00"
+ "time": "2015-06-14 21:17:01"
},
{
"name": "friendsofphp/php-cs-fixer",
@@ -4037,7 +4037,7 @@
}
],
"description": "A tool to automatically fix PHP code style",
- "time": "2017-03-31T12:59:38+00:00"
+ "time": "2017-03-31 12:59:38"
},
{
"name": "ircmaxell/password-compat",
@@ -4079,7 +4079,7 @@
"hashing",
"password"
],
- "time": "2014-11-20T16:49:30+00:00"
+ "time": "2014-11-20 16:49:30"
},
{
"name": "lusitanian/oauth",
@@ -4146,7 +4146,7 @@
"oauth",
"security"
],
- "time": "2016-07-12T22:15:40+00:00"
+ "time": "2016-07-12 22:15:40"
},
{
"name": "myclabs/deep-copy",
@@ -4191,7 +4191,7 @@
"object",
"object graph"
],
- "time": "2017-10-19T19:58:43+00:00"
+ "time": "2017-10-19 19:58:43"
},
{
"name": "pdepend/pdepend",
@@ -4231,7 +4231,7 @@
"BSD-3-Clause"
],
"description": "Official version of pdepend to be handled with Composer",
- "time": "2017-01-19T14:23:36+00:00"
+ "time": "2017-01-19 14:23:36"
},
{
"name": "phar-io/manifest",
@@ -4286,7 +4286,7 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
- "time": "2017-03-05T18:14:27+00:00"
+ "time": "2017-03-05 18:14:27"
},
{
"name": "phar-io/version",
@@ -4333,7 +4333,7 @@
}
],
"description": "Library for handling version information and constraints",
- "time": "2017-03-05T17:38:23+00:00"
+ "time": "2017-03-05 17:38:23"
},
{
"name": "phpdocumentor/reflection-common",
@@ -4387,20 +4387,20 @@
"reflection",
"static analysis"
],
- "time": "2017-09-11T18:02:19+00:00"
+ "time": "2017-09-11 18:02:19"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "4.2.0",
+ "version": "4.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "66465776cfc249844bde6d117abff1d22e06c2da"
+ "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da",
- "reference": "66465776cfc249844bde6d117abff1d22e06c2da",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
+ "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
"shasum": ""
},
"require": {
@@ -4438,7 +4438,7 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2017-11-27T17:38:31+00:00"
+ "time": "2017-11-30 07:14:17"
},
{
"name": "phpdocumentor/type-resolver",
@@ -4485,7 +4485,7 @@
"email": "me@mikevanriel.com"
}
],
- "time": "2017-07-14T14:27:02+00:00"
+ "time": "2017-07-14 14:27:02"
},
{
"name": "phpmd/phpmd",
@@ -4551,7 +4551,7 @@
"phpmd",
"pmd"
],
- "time": "2017-01-20T14:41:10+00:00"
+ "time": "2017-01-20 14:41:10"
},
{
"name": "phpspec/prophecy",
@@ -4614,7 +4614,7 @@
"spy",
"stub"
],
- "time": "2017-11-24T13:59:53+00:00"
+ "time": "2017-11-24 13:59:53"
},
{
"name": "phpunit/php-code-coverage",
@@ -4677,7 +4677,7 @@
"testing",
"xunit"
],
- "time": "2017-12-06T09:29:45+00:00"
+ "time": "2017-12-06 09:29:45"
},
{
"name": "phpunit/php-file-iterator",
@@ -4724,7 +4724,7 @@
"filesystem",
"iterator"
],
- "time": "2017-11-27T13:52:08+00:00"
+ "time": "2017-11-27 13:52:08"
},
{
"name": "phpunit/php-text-template",
@@ -4765,7 +4765,7 @@
"keywords": [
"template"
],
- "time": "2015-06-21T13:50:34+00:00"
+ "time": "2015-06-21 13:50:34"
},
{
"name": "phpunit/php-timer",
@@ -4814,7 +4814,7 @@
"keywords": [
"timer"
],
- "time": "2017-02-26T11:10:40+00:00"
+ "time": "2017-02-26 11:10:40"
},
{
"name": "phpunit/php-token-stream",
@@ -4863,7 +4863,7 @@
"keywords": [
"tokenizer"
],
- "time": "2017-11-27T05:48:46+00:00"
+ "time": "2017-11-27 05:48:46"
},
{
"name": "phpunit/phpunit",
@@ -4947,7 +4947,7 @@
"testing",
"xunit"
],
- "time": "2017-08-03T13:59:28+00:00"
+ "time": "2017-08-03 13:59:28"
},
{
"name": "phpunit/phpunit-mock-objects",
@@ -5006,7 +5006,7 @@
"mock",
"xunit"
],
- "time": "2017-08-03T14:08:16+00:00"
+ "time": "2017-08-03 14:08:16"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@@ -5051,7 +5051,7 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2017-03-04T06:30:41+00:00"
+ "time": "2017-03-04 06:30:41"
},
{
"name": "sebastian/comparator",
@@ -5115,7 +5115,7 @@
"compare",
"equality"
],
- "time": "2017-03-03T06:26:08+00:00"
+ "time": "2017-03-03 06:26:08"
},
{
"name": "sebastian/diff",
@@ -5167,7 +5167,7 @@
"keywords": [
"diff"
],
- "time": "2017-05-22T07:24:03+00:00"
+ "time": "2017-05-22 07:24:03"
},
{
"name": "sebastian/environment",
@@ -5217,7 +5217,7 @@
"environment",
"hhvm"
],
- "time": "2017-07-01T08:51:00+00:00"
+ "time": "2017-07-01 08:51:00"
},
{
"name": "sebastian/exporter",
@@ -5284,7 +5284,7 @@
"export",
"exporter"
],
- "time": "2017-04-03T13:19:02+00:00"
+ "time": "2017-04-03 13:19:02"
},
{
"name": "sebastian/finder-facade",
@@ -5323,7 +5323,7 @@
],
"description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
"homepage": "https://github.com/sebastianbergmann/finder-facade",
- "time": "2017-11-18T17:31:49+00:00"
+ "time": "2017-11-18 17:31:49"
},
{
"name": "sebastian/global-state",
@@ -5374,7 +5374,7 @@
"keywords": [
"global state"
],
- "time": "2017-04-27T15:39:26+00:00"
+ "time": "2017-04-27 15:39:26"
},
{
"name": "sebastian/object-enumerator",
@@ -5421,7 +5421,7 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
- "time": "2017-08-03T12:35:26+00:00"
+ "time": "2017-08-03 12:35:26"
},
{
"name": "sebastian/object-reflector",
@@ -5466,7 +5466,7 @@
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
- "time": "2017-03-29T09:07:27+00:00"
+ "time": "2017-03-29 09:07:27"
},
{
"name": "sebastian/phpcpd",
@@ -5517,7 +5517,7 @@
],
"description": "Copy/Paste Detector (CPD) for PHP code.",
"homepage": "https://github.com/sebastianbergmann/phpcpd",
- "time": "2016-04-17T19:32:49+00:00"
+ "time": "2016-04-17 19:32:49"
},
{
"name": "sebastian/recursion-context",
@@ -5570,7 +5570,7 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2017-03-03T06:23:57+00:00"
+ "time": "2017-03-03 06:23:57"
},
{
"name": "sebastian/resource-operations",
@@ -5612,7 +5612,7 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2015-07-28T20:34:47+00:00"
+ "time": "2015-07-28 20:34:47"
},
{
"name": "sebastian/version",
@@ -5655,7 +5655,7 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
- "time": "2016-10-03T07:35:21+00:00"
+ "time": "2016-10-03 07:35:21"
},
{
"name": "squizlabs/php_codesniffer",
@@ -5706,20 +5706,20 @@
"phpcs",
"standards"
],
- "time": "2017-06-14T01:23:49+00:00"
+ "time": "2017-06-14 01:23:49"
},
{
"name": "symfony/config",
- "version": "v3.4.3",
+ "version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "cfd5c972f7b4992a5df41673d25d980ab077aa5b"
+ "reference": "72689b934d6c6ecf73eca874e98933bf055313c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/cfd5c972f7b4992a5df41673d25d980ab077aa5b",
- "reference": "cfd5c972f7b4992a5df41673d25d980ab077aa5b",
+ "url": "https://api.github.com/repos/symfony/config/zipball/72689b934d6c6ecf73eca874e98933bf055313c9",
+ "reference": "72689b934d6c6ecf73eca874e98933bf055313c9",
"shasum": ""
},
"require": {
@@ -5768,20 +5768,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:37:34+00:00"
+ "time": "2018-01-21 19:05:02"
},
{
"name": "symfony/dependency-injection",
- "version": "v3.4.3",
+ "version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "35f957ca171a431710966bec6e2f8636d3b019c4"
+ "reference": "4b2717ee2499390e371e1fc7abaf886c1c83e83d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/35f957ca171a431710966bec6e2f8636d3b019c4",
- "reference": "35f957ca171a431710966bec6e2f8636d3b019c4",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4b2717ee2499390e371e1fc7abaf886c1c83e83d",
+ "reference": "4b2717ee2499390e371e1fc7abaf886c1c83e83d",
"shasum": ""
},
"require": {
@@ -5839,20 +5839,20 @@
],
"description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com",
- "time": "2018-01-04T15:56:45+00:00"
+ "time": "2018-01-29 09:16:57"
},
{
"name": "symfony/polyfill-php54",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php54.git",
- "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc"
+ "reference": "84e2b616c197ef400c6d0556a0606cee7c9e21d5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/d7810a14b2c6c1aff415e1bb755f611b3d5327bc",
- "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc",
+ "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/84e2b616c197ef400c6d0556a0606cee7c9e21d5",
+ "reference": "84e2b616c197ef400c6d0556a0606cee7c9e21d5",
"shasum": ""
},
"require": {
@@ -5861,7 +5861,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -5897,20 +5897,20 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-30 19:27:44"
},
{
"name": "symfony/polyfill-php55",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php55.git",
- "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd"
+ "reference": "168371cb3dfb10e0afde96e7c2688be02470d143"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/b64e7f0c37ecf144ecc16668936eef94e628fbfd",
- "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd",
+ "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/168371cb3dfb10e0afde96e7c2688be02470d143",
+ "reference": "168371cb3dfb10e0afde96e7c2688be02470d143",
"shasum": ""
},
"require": {
@@ -5920,7 +5920,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -5953,20 +5953,20 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-30 19:27:44"
},
{
"name": "symfony/polyfill-php70",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php70.git",
- "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff"
+ "reference": "3532bfcd8f933a7816f3a0a59682fc404776600f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff",
- "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff",
+ "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/3532bfcd8f933a7816f3a0a59682fc404776600f",
+ "reference": "3532bfcd8f933a7816f3a0a59682fc404776600f",
"shasum": ""
},
"require": {
@@ -5976,7 +5976,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -6012,20 +6012,20 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-30 19:27:44"
},
{
"name": "symfony/polyfill-php72",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254"
+ "reference": "8eca20c8a369e069d4f4c2ac9895144112867422"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/6de4f4884b97abbbed9f0a84a95ff2ff77254254",
- "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8eca20c8a369e069d4f4c2ac9895144112867422",
+ "reference": "8eca20c8a369e069d4f4c2ac9895144112867422",
"shasum": ""
},
"require": {
@@ -6034,7 +6034,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -6067,20 +6067,20 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-31 17:43:24"
},
{
"name": "symfony/polyfill-xml",
- "version": "v1.6.0",
+ "version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-xml.git",
- "reference": "d7bcb5c3bb1832c532379df50825c08f43a64134"
+ "reference": "fcdfb6e64d21848ee65b6d5d0bc75fcb703b0c83"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/d7bcb5c3bb1832c532379df50825c08f43a64134",
- "reference": "d7bcb5c3bb1832c532379df50825c08f43a64134",
+ "url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/fcdfb6e64d21848ee65b6d5d0bc75fcb703b0c83",
+ "reference": "fcdfb6e64d21848ee65b6d5d0bc75fcb703b0c83",
"shasum": ""
},
"require": {
@@ -6090,7 +6090,7 @@
"type": "metapackage",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -6115,11 +6115,11 @@
"portable",
"shim"
],
- "time": "2017-10-11T12:05:26+00:00"
+ "time": "2018-01-30 19:27:44"
},
{
"name": "symfony/stopwatch",
- "version": "v3.4.3",
+ "version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
@@ -6164,7 +6164,7 @@
],
"description": "Symfony Stopwatch Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:37:34+00:00"
+ "time": "2018-01-03 07:37:34"
},
{
"name": "theseer/fdomdocument",
@@ -6204,7 +6204,7 @@
],
"description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
"homepage": "https://github.com/theseer/fDOMDocument",
- "time": "2017-06-30T11:53:12+00:00"
+ "time": "2017-06-30 11:53:12"
},
{
"name": "theseer/tokenizer",
@@ -6244,20 +6244,20 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
- "time": "2017-04-07T12:08:54+00:00"
+ "time": "2017-04-07 12:08:54"
},
{
"name": "webmozart/assert",
- "version": "1.2.0",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
- "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f"
+ "reference": "0df1908962e7a3071564e857d86874dad1ef204a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f",
- "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
+ "reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"shasum": ""
},
"require": {
@@ -6294,7 +6294,7 @@
"check",
"validate"
],
- "time": "2016-11-23T20:04:58+00:00"
+ "time": "2018-01-29 19:49:41"
}
],
"aliases": [],
diff --git a/dev/tests/functional/credentials.xml.dist b/dev/tests/functional/credentials.xml.dist
index 57e24cd59aa08..01e3a35be9a2d 100644
--- a/dev/tests/functional/credentials.xml.dist
+++ b/dev/tests/functional/credentials.xml.dist
@@ -80,4 +80,5 @@
+
diff --git a/dev/tests/functional/etc/repository_replacer_payments.xml b/dev/tests/functional/etc/repository_replacer_payments.xml
index 2b20e7220f537..a0ecca61eb372 100644
--- a/dev/tests/functional/etc/repository_replacer_payments.xml
+++ b/dev/tests/functional/etc/repository_replacer_payments.xml
@@ -21,4 +21,11 @@
AUTHORIZENET_PASSWORD
+
+
+
+ SIGNIFYD_EMAIL
+ SIGNIFYD_PASSWORD
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/Grid.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/Grid.php
new file mode 100644
index 0000000000000..54f580f48dcf4
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/Grid.php
@@ -0,0 +1,33 @@
+ [
+ 'selector' => '[name="increment_id"]',
+ ],
+ 'status' => [
+ 'selector' => '[name="status"]',
+ 'input' => 'select',
+ ],
+ 'signifyd_guarantee_status' => [
+ 'selector' => '[name="signifyd_guarantee_status"]',
+ 'input' => 'select'
+ ]
+ ];
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/View/FraudProtection.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/View/FraudProtection.php
new file mode 100644
index 0000000000000..e2b4e6f748977
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/Adminhtml/Order/View/FraudProtection.php
@@ -0,0 +1,31 @@
+_rootElement->find($this->caseGuaranteeDisposition)->getText();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseInfo.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseInfo.php
new file mode 100644
index 0000000000000..5fe6096035803
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseInfo.php
@@ -0,0 +1,245 @@
+_rootElement->find($this->noDeviceAnalysisAvailable)->isVisible();
+ }
+
+ /**
+ * Returns shipping price.
+ *
+ * @return string
+ */
+ public function getShippingPrice()
+ {
+ return $this->_rootElement->find($this->shippingPrice)->getText();
+ }
+
+ /**
+ * Flags case as good or bad.
+ *
+ * @param string $flagType
+ * @return void
+ */
+ public function flagCase($flagType)
+ {
+ $flagSelector = ($flagType === 'Good')
+ ? $this->flagCaseAsGoodButton
+ : $this->flagCaseAsBadButton;
+
+ $this->_rootElement->find($flagSelector)->click();
+ }
+
+ /**
+ * Flags case as bad.
+ *
+ * @return void
+ */
+ public function flagCaseAsBad()
+ {
+ $this->_rootElement->find($this->flagCaseAsBadButton)->click();
+ }
+
+ /**
+ * Gets guarantee disposition.
+ *
+ * @return string
+ */
+ public function getGuaranteeDisposition()
+ {
+ return $this->_rootElement->find($this->guaranteeDisposition)->getText();
+ }
+
+ /**
+ * Gets CVV response.
+ *
+ * @return string
+ */
+ public function getCvvResponse()
+ {
+ return sprintf(
+ '%s (%s)',
+ $this->_rootElement->find($this->cvvResponseDescription)->getText(),
+ $this->_rootElement->find($this->cvvResponseCode)->getText()
+ );
+ }
+
+ /**
+ * Gets AVS response.
+ *
+ * @return string
+ */
+ public function getAvsResponse()
+ {
+ return sprintf(
+ '%s (%s)',
+ $this->_rootElement->find($this->avsResponseDescription)->getText(),
+ $this->_rootElement->find($this->avsResponseCode)->getText()
+ );
+ }
+
+ /**
+ * Gets displayed order id.
+ *
+ * @return string
+ */
+ public function getOrderId()
+ {
+ return $this->_rootElement->find($this->orderId)->getText();
+ }
+
+ /**
+ * Gets displayed order amount.
+ *
+ * @return string
+ */
+ public function getOrderAmount()
+ {
+ return $this->_rootElement->find($this->orderAmount)->getText();
+ }
+
+ /**
+ * Returns displayed order amount currency.
+ *
+ * @return string
+ */
+ public function getOrderAmountCurrency()
+ {
+ return $this->_rootElement->find($this->orderAmountCurrency)->getText();
+ }
+
+ /**
+ * Gets displayed card holder name.
+ *
+ * @return string
+ */
+ public function getCardHolder()
+ {
+ return $this->_rootElement->find($this->cardHolder)->getText();
+ }
+
+ /**
+ * Gets displayed billing address.
+ *
+ * @return string
+ */
+ public function getBillingAddress()
+ {
+ return $this->_rootElement->find($this->billingAddress)->getText();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseSearch.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseSearch.php
new file mode 100644
index 0000000000000..ef292de3a9e5f
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/CaseSearch.php
@@ -0,0 +1,67 @@
+_rootElement->find($this->searchBar)->setValue($customerName);
+ $this->_rootElement->find($this->submitButton)->click();
+ }
+
+ /**
+ * Select searched case.
+ *
+ * @return void
+ */
+ public function selectCase()
+ {
+ $this->_rootElement->find($this->selectCaseLink)->click();
+ }
+
+ /**
+ * Waiting of case page loading.
+ *
+ * @return void
+ */
+ public function waitForLoading()
+ {
+ $this->waitForElementVisible($this->searchBar);
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/SignifydLogin.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/SignifydLogin.php
new file mode 100644
index 0000000000000..7f530afe4df63
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/SignifydLogin.php
@@ -0,0 +1,31 @@
+_rootElement->find($this->loginButton)->click();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/Webhooks.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/Webhooks.php
new file mode 100644
index 0000000000000..424d5681c07c6
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Block/SignifydConsole/Webhooks.php
@@ -0,0 +1,216 @@
+ 'Case Creation',
+ 'CASE_REVIEW' => 'Case Review',
+ 'GUARANTEE_COMPLETION' => 'Guarantee Completion'
+ ];
+
+ /**
+ * XPath selector of webhook element added into grid.
+ *
+ * @var string
+ */
+ private $webhookAddedElement = '//table[@id="webhooks"]//tr[./td/span/text()="%s" and ./td/span/text()="%s"]';
+
+ /**
+ * Css selector of webhook url input.
+ *
+ * @var string
+ */
+ private $webhookUrl = '[id="webhookUrl"]';
+
+ /**
+ * XPath selector of test team select option.
+ *
+ * @var string
+ */
+ private $webhookTeamOption = './/select[@id="webhookTeams"]//option[text()="%s"]';
+
+ /**
+ * Css selector of webhook event select option.
+ *
+ * @var string
+ */
+ private $webhookEventOption = 'select[id="webhookEvent"] option[value="%s"]';
+
+ /**
+ * Css selector of webhook addition button.
+ *
+ * @var string
+ */
+ private $webhookAddButton = '[id="addWebhook"] [type=submit]';
+
+ /**
+ * Css selector of delete button in element of webhook grid.
+ *
+ * @var string
+ */
+ private $webhookDeleteButton = '[class*="webhook-delete"]';
+
+ /**
+ * Css selector of confirming button for deleting webhook.
+ *
+ * @var string
+ */
+ private $webhookDeleteConfirmButton = '[class="appriseOuter"] button[value="ok"]';
+
+ /**
+ * Creates new set of webhooks, if it not exists.
+ *
+ * @param string $team
+ * @return void
+ */
+ public function create($team)
+ {
+ $handlerUrl = $this->getHandlerUrl();
+
+ foreach ($this->webhookEventOptionsMap as $webhookEventCode => $webhookEventName) {
+ if ($this->getWebhook($team, $webhookEventName)) {
+ continue;
+ }
+
+ $this->addWebhook($handlerUrl, $webhookEventCode, $team);
+ }
+ }
+
+ /**
+ * Deletes set of webhooks.
+ *
+ * @param string $team
+ * @return void
+ */
+ public function cleanup($team)
+ {
+ foreach ($this->webhookEventOptionsMap as $webhookEventName) {
+ if ($webhook = $this->getWebhook($team, $webhookEventName)) {
+ $this->deleteWebhook($webhook);
+ }
+ }
+ }
+
+ /**
+ * Gets webhook if exists.
+ *
+ * @param string $team
+ * @param string $webhookEventName
+ * @return ElementInterface|null
+ */
+ private function getWebhook($team, $webhookEventName)
+ {
+ $webhook = $this->_rootElement->find(
+ sprintf($this->webhookAddedElement, $team, $webhookEventName),
+ Locator::SELECTOR_XPATH
+ );
+
+ return $webhook->isPresent() ? $webhook : null;
+ }
+
+ /**
+ * Delete webhook element with confirmation popup.
+ *
+ * @param ElementInterface $webhook
+ * @return void
+ */
+ private function deleteWebhook(ElementInterface $webhook)
+ {
+ $webhook->find($this->webhookDeleteButton)->click();
+ $this->_rootElement->find($this->webhookDeleteConfirmButton)->click();
+ }
+
+ /**
+ * Sets webhook data and add it.
+ *
+ * @param string $handlerUrl
+ * @param string $webhookEventCode
+ * @param string $team
+ * @return void
+ */
+ private function addWebhook(
+ $handlerUrl,
+ $webhookEventCode,
+ $team
+ ) {
+ $this->setEvent($webhookEventCode);
+ $this->setTeam($team);
+ $this->setUrl($handlerUrl);
+ $this->submit();
+ }
+
+ /**
+ * Sets appropriate webhook event select option by code.
+ *
+ * @param string $webhookEventCode
+ * @return void
+ */
+ private function setEvent($webhookEventCode)
+ {
+ $this->_rootElement->find(
+ sprintf($this->webhookEventOption, $webhookEventCode)
+ )->click();
+ }
+
+ /**
+ * Sets test team select option.
+ *
+ * @param string $team
+ * @return void
+ */
+ private function setTeam($team)
+ {
+ $this->_rootElement->find(
+ sprintf($this->webhookTeamOption, $team),
+ Locator::SELECTOR_XPATH
+ )->click();
+ }
+
+ /**
+ * Sets webhook handler url input value.
+ *
+ * @param string $handlerUrl
+ * @return void
+ */
+ private function setUrl($handlerUrl)
+ {
+ $this->_rootElement->find($this->webhookUrl)->setValue($handlerUrl);
+ }
+
+ /**
+ * Add webhook element.
+ *
+ * @return void
+ */
+ private function submit()
+ {
+ $this->_rootElement->find($this->webhookAddButton)->click();
+ }
+
+ /**
+ * Gets webhook handler url.
+ *
+ * @return string
+ */
+ private function getHandlerUrl()
+ {
+ return $_ENV['app_frontend_url'] . 'signifyd/webhooks/handler';
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertAwaitingSignifydGuaranteeInCommentsHistory.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertAwaitingSignifydGuaranteeInCommentsHistory.php
new file mode 100644
index 0000000000000..55134b8d94548
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertAwaitingSignifydGuaranteeInCommentsHistory.php
@@ -0,0 +1,73 @@
+open();
+ $salesOrder->getSalesOrderGrid()->searchAndOpen(['id' => $orderId]);
+
+ /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */
+ $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info');
+ $orderComments = $infoTab->getCommentsHistoryBlock()->getComments();
+
+ $key = array_search(
+ $this->historyComment,
+ array_column($orderComments, 'comment')
+ );
+
+ \PHPUnit_Framework_Assert::assertNotFalse(
+ $key,
+ 'There is no message about awaiting the Signifyd guarantee disposition' .
+ ' in Comments History section for the order #' . $orderId
+ );
+
+ \PHPUnit_Framework_Assert::assertEquals(
+ $this->historyCommentStatus,
+ $orderComments[$key]['status'],
+ 'Message about awaiting the Signifyd guarantee disposition' .
+ ' doesn\'t have status "'. $this->historyCommentStatus.'"' .
+ ' in Comments History section for the order #' . $orderId
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return "Message about awaiting the Signifyd guarantee disposition is available in Comments History section.";
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnAdmin.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnAdmin.php
new file mode 100644
index 0000000000000..9fe29f022470f
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnAdmin.php
@@ -0,0 +1,85 @@
+open();
+ $salesOrder->getSalesOrderGrid()->searchAndOpen(['id' => $orderId]);
+
+ $this->orderView = $orderView;
+ $this->signifydData = $signifydData;
+ $this->orderId = $orderId;
+
+ $this->checkCaseGuaranteeDisposition();
+ }
+
+ /**
+ * Checks case guarantee disposition is correct.
+ *
+ * @return void
+ */
+ private function checkCaseGuaranteeDisposition()
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $this->signifydData->getGuaranteeDisposition(),
+ $this->orderView->getFraudProtectionBlock()->getCaseGuaranteeDisposition(),
+ 'Case Guarantee Disposition status is wrong for order #' . $this->orderId
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return 'Signifyd Case information is correct in Admin.';
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnSignifydConsole.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnSignifydConsole.php
new file mode 100644
index 0000000000000..995f5092b6121
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertCaseInfoOnSignifydConsole.php
@@ -0,0 +1,214 @@
+signifydCases = $signifydCases;
+
+ $this->checkDeviceData();
+ $this->checkShippingPrice($signifydData->getShippingPrice());
+ $this->checkGuaranteeDisposition($signifydData->getGuaranteeDisposition());
+ $cvvResponse = $signifydData->getCvvResponse();
+ if (isset($cvvResponse)) {
+ $this->checkCvvResponse($cvvResponse);
+ }
+ $this->checkAvsResponse($signifydData->getAvsResponse());
+ $this->checkOrderId($orderId);
+ $this->checkOrderAmount($prices['grandTotal']);
+ $this->checkOrderAmountCurrency($prices['grandTotalCurrency']);
+ $this->checkCardHolder($customerFullName);
+ $this->checkBillingAddress($billingAddress);
+ }
+
+ /**
+ * Checks device data are present.
+ *
+ * @return void
+ */
+ private function checkDeviceData()
+ {
+ \PHPUnit_Framework_Assert::assertTrue(
+ $this->signifydCases->getCaseInfoBlock()->isAvailableDeviceData(),
+ 'Device data are not available on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks shipping price is correct.
+ *
+ * @param string $shippingPrice
+ * @return void
+ */
+ private function checkShippingPrice($shippingPrice)
+ {
+ \PHPUnit_Framework_Assert::assertContains(
+ $shippingPrice,
+ $this->signifydCases->getCaseInfoBlock()->getShippingPrice(),
+ 'Shipping price is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks guarantee disposition is correct.
+ *
+ * @param string $guaranteeDisposition
+ * @return void
+ */
+ private function checkGuaranteeDisposition($guaranteeDisposition)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $guaranteeDisposition,
+ $this->signifydCases->getCaseInfoBlock()->getGuaranteeDisposition(),
+ 'Guarantee disposition is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks CVV response is correct.
+ *
+ * @param string $cvvResponse
+ * @return void
+ */
+ private function checkCvvResponse($cvvResponse)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $cvvResponse,
+ $this->signifydCases->getCaseInfoBlock()->getCvvResponse(),
+ 'CVV response is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks AVS response is correct.
+ *
+ * @param string $avsResponse
+ * @return void
+ */
+ private function checkAvsResponse($avsResponse)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $avsResponse,
+ $this->signifydCases->getCaseInfoBlock()->getAvsResponse(),
+ 'AVS response is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks order id is correct.
+ *
+ * @param string $orderId
+ * @return void
+ */
+ private function checkOrderId($orderId)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $orderId,
+ $this->signifydCases->getCaseInfoBlock()->getOrderId(),
+ 'Order id is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks order amount is correct.
+ *
+ * @param string $amount
+ * @return void
+ */
+ private function checkOrderAmount($amount)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ number_format($amount, 2),
+ $this->signifydCases->getCaseInfoBlock()->getOrderAmount(),
+ 'Order amount is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks order amount currency is correct.
+ *
+ * @param string $currency
+ * @return void
+ */
+ private function checkOrderAmountCurrency($currency)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $currency,
+ $this->signifydCases->getCaseInfoBlock()->getOrderAmountCurrency(),
+ 'Order amount currency is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks card holder is correct.
+ *
+ * @param string $customerFullName
+ * @return void
+ */
+ private function checkCardHolder($customerFullName)
+ {
+ \PHPUnit_Framework_Assert::assertEquals(
+ $customerFullName,
+ $this->signifydCases->getCaseInfoBlock()->getCardHolder(),
+ 'Card holder name is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * Checks billing address is correct.
+ *
+ * @param SignifydAddress $billingAddress
+ * @return void
+ */
+ private function checkBillingAddress(SignifydAddress $billingAddress)
+ {
+ \PHPUnit_Framework_Assert::assertContains(
+ $billingAddress->getStreet(),
+ $this->signifydCases->getCaseInfoBlock()->getBillingAddress(),
+ 'Billing address is incorrect on case page in Signifyd console.'
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return 'Case information is correct on case page in Signifyd console.';
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInCommentsHistory.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInCommentsHistory.php
new file mode 100644
index 0000000000000..aef5c0e9abd26
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInCommentsHistory.php
@@ -0,0 +1,56 @@
+open();
+ $salesOrder->getSalesOrderGrid()->searchAndOpen(['id' => $orderId]);
+
+ /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */
+ $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info');
+ $orderComments = $infoTab->getCommentsHistoryBlock()->getComments();
+ $commentsMessages = array_column($orderComments, 'comment');
+
+ \PHPUnit_Framework_Assert::assertRegExp(
+ self::CASE_CREATED_PATTERN,
+ implode('. ', $commentsMessages),
+ 'Signifyd case is not created for the order #' . $orderId
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return "Message about Signifyd Case is available in Comments History section.";
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInOrdersGrid.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInOrdersGrid.php
new file mode 100644
index 0000000000000..2f08ac8f40a3e
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydCaseInOrdersGrid.php
@@ -0,0 +1,50 @@
+ $orderId,
+ 'signifyd_guarantee_status' => $signifydData->getGuaranteeDisposition()
+ ];
+
+ $errorMessage = implode(', ', $filter);
+
+ $ordersGrid->open();
+
+ \PHPUnit_Framework_Assert::assertTrue(
+ $ordersGrid->getSignifydOrdersGrid()->isRowVisible(array_filter($filter)),
+ 'Order with following data \'' . $errorMessage . '\' is absent in orders grid.'
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return 'Signifyd guarantee status is displayed in sales orders grid.';
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydGuaranteeCancelInCommentsHistory.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydGuaranteeCancelInCommentsHistory.php
new file mode 100644
index 0000000000000..4b86f66730f90
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Constraint/AssertSignifydGuaranteeCancelInCommentsHistory.php
@@ -0,0 +1,57 @@
+open();
+ $salesOrder->getSalesOrderGrid()->searchAndOpen(['id' => $orderId]);
+
+ /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */
+ $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info');
+ $orderComments = $infoTab->getCommentsHistoryBlock()->getComments();
+ $commentsMessages = array_column($orderComments, 'comment');
+
+ \PHPUnit_Framework_Assert::assertContains(
+ $this->guaranteeCancelMessage,
+ implode('. ', $commentsMessages),
+ 'There is no message regarding Signifyd guarantee cancel in Comments History section for the order #'
+ . $orderId
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toString()
+ {
+ return "Message about Signifyd guarantee cancel is available in Comments History section.";
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAccount.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAccount.xml
new file mode 100644
index 0000000000000..8b1455811d003
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAccount.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress.xml
new file mode 100644
index 0000000000000..02b5cc8211d89
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress/Firstname.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress/Firstname.php
new file mode 100644
index 0000000000000..9e4930484eef0
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydAddress/Firstname.php
@@ -0,0 +1,46 @@
+data = $data;
+ }
+
+ /**
+ * Add isolation for `firstname` field.
+ *
+ * @param null $key
+ * @return string
+ */
+ public function getData($key = null)
+ {
+ $this->data = str_replace('%signifyd_isolation%', $this->generateIsolation(), $this->data);
+
+ return parent::getData($key);
+ }
+
+ /**
+ * Generates character isolation.
+ *
+ * @param int $length
+ * @return string
+ */
+ private function generateIsolation($length = 10)
+ {
+ return substr(str_shuffle(str_repeat("abcdefghijklmnopqrstuvwxyz", $length)), 0, $length);
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydData.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydData.xml
new file mode 100644
index 0000000000000..23ee2cddf434a
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Fixture/SignifydData.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/OrdersGrid.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/OrdersGrid.xml
new file mode 100644
index 0000000000000..749d91c3ed395
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/OrdersGrid.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/SalesOrderView.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/SalesOrderView.xml
new file mode 100644
index 0000000000000..36d77723dfcee
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/Adminhtml/SalesOrderView.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydCases.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydCases.xml
new file mode 100644
index 0000000000000..cfe889bb8de44
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydCases.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydLogin.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydLogin.xml
new file mode 100644
index 0000000000000..5fe5da6d28013
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydLogin.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydNotifications.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydNotifications.xml
new file mode 100644
index 0000000000000..dd12f14c28800
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Page/SignifydConsole/SignifydNotifications.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Address.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Address.xml
new file mode 100644
index 0000000000000..a534cbc6107fe
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Address.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ John%signifyd_isolation%
+ Doe
+ Magento
+ Culver City
+ 6161 West Centinela Avenue
+ 555-55-555-55
+ United States
+ California
+ 90230
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/ConfigData.xml
new file mode 100644
index 0000000000000..2af46f077b304
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/ConfigData.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ - fraud_protection
+ - 1
+ - Yes
+ - 1
+
+
+ - fraud_protection
+ - 1
+
+ - SIGNIFYD_CONFIG_API_KEY
+
+
+ - fraud_protection
+ - 1
+
+ - https://api.signifyd.com/v2/
+
+
+ - fraud_protection
+ - 1
+ - Yes
+ - 1
+
+
+
+
+ - fraud_protection
+ - 1
+ - Yes
+ - 0
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Customer.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Customer.xml
new file mode 100644
index 0000000000000..b45e3d01b8ec8
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/Customer.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ John
+ Doe
+ testapprove@magento.com
+ 123123^q
+ 123123^q
+
+
+ John
+ Doe
+ testdecline@magento.com
+ 123123^q
+ 123123^q
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydAccount.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydAccount.xml
new file mode 100644
index 0000000000000..4d3dd5229eae3
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydAccount.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ SIGNIFYD_EMAIL
+ SIGNIFYD_PASSWORD
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydData.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydData.xml
new file mode 100644
index 0000000000000..5b3be4b9d570a
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/Repository/SignifydData.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ autotest
+ Good
+ Approved
+ CVV2 Match (M)
+ Full match (Y)
+ USD 5.00
+
+
+ autotest
+ Bad
+ Declined
+ CVV2 Match (M)
+ Full match (Y)
+ USD 5.00
+
+
+ autotest
+ Bad
+ Declined
+ Unavailable (U)
+ GBP 10.00
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.php
new file mode 100644
index 0000000000000..1dd742c9f7096
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.php
@@ -0,0 +1,63 @@
+executeScenario();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.xml
new file mode 100644
index 0000000000000..6391081e5e161
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/AcceptPaymentWithSignifydGuaranteeDeclinedTest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ catalogProductSimple::product_100_dollar
+ signifyd_decline_us_customer
+ login
+ Flat Rate
+ Fixed
+ hosted_pro
+
+ - 210.00
+ - GBP
+
+ credit_card_hostedpro
+ visa_hosted_pro
+ false
+ merchant_country_gb, hosted_pro, config_base_currency_gb, signifyd
+ Suspected Fraud
+ signifyd_us_shipping_address
+ sandbox_default
+ signifyd_guarantee_fraudulent
+ Processing
+ test_type:3rd_party_test_single_flow, severity:S2
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.php
new file mode 100644
index 0000000000000..5ca76ff70479f
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.php
@@ -0,0 +1,62 @@
+executeScenario();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.xml
new file mode 100644
index 0000000000000..404229c2451cd
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/CreateSignifydGuaranteeAndCancelOrderTest.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ catalogProductSimple::product_10_dollar
+ login
+ signifyd_approve_us_customer
+ Flat Rate
+ Fixed
+ braintree
+ braintree
+ 15.00
+ USD
+ visa_default
+ braintree
+ braintree,signifyd
+ Processing
+ signifyd_us_shipping_address
+ sandbox_default
+ signifyd_guarantee_approve
+ test_type:3rd_party_test_single_flow, severity:S1
+
+
+
+
+
+
+
+
+
+ catalogProductSimple::product_10_dollar
+ login
+ signifyd_decline_us_customer
+ Flat Rate
+ Fixed
+ braintree
+ braintree
+ 15.00
+ USD
+ visa_default
+ braintree
+ braintree,signifyd
+ On Hold
+ signifyd_us_shipping_address
+ sandbox_default
+ signifyd_guarantee_decline
+ test_type:3rd_party_test_single_flow, severity:S1
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.php
new file mode 100644
index 0000000000000..698861da26018
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.php
@@ -0,0 +1,63 @@
+executeScenario();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.xml
new file mode 100644
index 0000000000000..5496619fe1bf9
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestCase/DenyPaymentWithSignifydGuaranteeDeclinedTest.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ catalogProductSimple::product_100_dollar
+ signifyd_decline_us_customer
+ login
+ Flat Rate
+ Fixed
+ hosted_pro
+
+ - 210.00
+ - GBP
+
+ credit_card_hostedpro
+ visa_hosted_pro
+ false
+ merchant_country_gb, hosted_pro, config_base_currency_gb, signifyd
+ Suspected Fraud
+ Canceled
+ signifyd_us_shipping_address
+ sandbox_default
+ signifyd_guarantee_fraudulent
+ test_type:3rd_party_test_single_flow, severity:S2
+
+
+
+
+
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/OpenOrderGridStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/OpenOrderGridStep.php
new file mode 100644
index 0000000000000..409dffc8340b7
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/OpenOrderGridStep.php
@@ -0,0 +1,173 @@
+placeOrderStatus = $placeOrderStatus;
+ $this->orderId = $orderId;
+ $this->orderIndex = $orderIndex;
+ $this->salesOrderView = $salesOrderView;
+ $this->ordersGrid = $ordersGrid;
+ $this->assertOrderStatus = $assertOrderStatus;
+ $this->assertCaseInfo = $assertCaseInfo;
+ $this->assertOrdersGrid = $assertOrdersGrid;
+ $this->signifydData = $signifydData;
+ }
+
+ /**
+ * Open order.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ $this->checkOrdersGrid();
+ $this->checkCaseInfo();
+ $this->checkOrderStatus();
+ }
+
+ /**
+ * Run assert to check Signifyd Case Disposition status in orders grid.
+ *
+ * @return void
+ */
+ private function checkOrdersGrid()
+ {
+ $this->assertOrdersGrid->processAssert(
+ $this->orderId,
+ $this->ordersGrid,
+ $this->signifydData
+ );
+ }
+
+ /**
+ * Run assert to check order status is valid.
+ *
+ * @return void
+ */
+ private function checkOrderStatus()
+ {
+ $this->assertOrderStatus->processAssert(
+ $this->placeOrderStatus,
+ $this->orderId,
+ $this->orderIndex,
+ $this->salesOrderView
+ );
+ }
+
+ /**
+ * Run assert to check Signifyd Case information is correct in Admin.
+ *
+ * @return void
+ */
+ private function checkCaseInfo()
+ {
+ $this->assertCaseInfo->processAssert(
+ $this->salesOrderView,
+ $this->orderIndex,
+ $this->signifydData,
+ $this->orderId
+ );
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCancelOrderStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCancelOrderStep.php
new file mode 100644
index 0000000000000..54f4dd75223e0
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCancelOrderStep.php
@@ -0,0 +1,105 @@
+orderIndex = $orderIndex;
+ $this->order = $order;
+ $this->salesOrderView = $salesOrderView;
+ $this->testStepFactory = $testStepFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ $this->orderIndex->open();
+ $this->orderIndex->getSalesOrderGrid()
+ ->searchAndOpen(['id' => $this->order->getId()]);
+
+ switch ($this->salesOrderView->getOrderInfoBlock()->getOrderStatus()) {
+ case 'Suspected Fraud':
+ $this->getStepInstance(DenyPaymentStep::class)->run();
+ break;
+ case 'On Hold':
+ $this->getStepInstance(UnholdOrderStep::class)->run();
+ $this->getStepInstance(CancelOrderStep::class)->run();
+ break;
+ case 'Canceled':
+ break;
+ default:
+ $this->getStepInstance(CancelOrderStep::class)->run();
+ }
+ }
+
+ /**
+ * Creates test step instance with preset params.
+ *
+ * @param string $class
+ * @return TestStepInterface
+ */
+ private function getStepInstance($class)
+ {
+ return $this->testStepFactory->create(
+ $class,
+ ['order' => $this->order]
+ );
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCreateCustomerStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCreateCustomerStep.php
new file mode 100644
index 0000000000000..5fec5988d2c9f
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydCreateCustomerStep.php
@@ -0,0 +1,77 @@
+customer = $customer;
+ $this->testStepFactory = $testStepFactory;
+ }
+
+ /**
+ * Run step flow.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ $this->getStepInstance(CreateCustomerStep::class)->run();
+ }
+
+ /**
+ * @return void
+ */
+ public function cleanup()
+ {
+ $this->getStepInstance(CreateCustomerStep::class)->cleanup();
+ $this->getStepInstance(DeleteCustomerStep::class)->run();
+ }
+
+ /**
+ * Creates test step instance with preset params.
+ *
+ * @param string $class
+ * @return TestStepInterface
+ */
+ private function getStepInstance($class)
+ {
+ return $this->testStepFactory->create(
+ $class,
+ ['customer' => $this->customer]
+ );
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydFillShippingAddressStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydFillShippingAddressStep.php
new file mode 100644
index 0000000000000..0b1dda1d0a46a
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydFillShippingAddressStep.php
@@ -0,0 +1,70 @@
+signifydAddress = $signifydAddress;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ parent::run();
+
+ return [
+ 'signifydAddress' => $this->signifydAddress,
+ ];
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydLoginStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydLoginStep.php
new file mode 100644
index 0000000000000..3dd1b94d6b505
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydLoginStep.php
@@ -0,0 +1,68 @@
+signifydAccount = $signifydAccount;
+ $this->signifydLogin = $signifydLogin;
+ $this->signifydCases = $signifydCases;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ $this->signifydLogin->open();
+
+ if ($this->signifydLogin->getLoginBlock()->isVisible()) {
+ $this->signifydLogin->getLoginBlock()->fill($this->signifydAccount);
+ $this->signifydLogin->getLoginBlock()->login();
+ }
+
+ $this->signifydCases->getCaseSearchBlock()->waitForLoading();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydObserveCaseStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydObserveCaseStep.php
new file mode 100644
index 0000000000000..126e32489e6c9
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydObserveCaseStep.php
@@ -0,0 +1,152 @@
+assertCaseInfo = $assertCaseInfoOnSignifydConsole;
+ $this->signifydAddress = $signifydAddress;
+ $this->signifydCases = $signifydCases;
+ $this->signifydNotifications = $signifydNotifications;
+ $this->signifydData = $signifydData;
+ $this->order = $order;
+ $this->testStepFactory = $testStepFactory;
+ $this->prices = $prices;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ $this->signifydCases->open();
+ $this->signifydCases->getCaseSearchBlock()
+ ->searchCaseByCustomerName($this->signifydAddress->getFirstname());
+ $this->signifydCases->getCaseSearchBlock()->selectCase();
+ $this->signifydCases->getCaseInfoBlock()->flagCase($this->signifydData->getCaseFlag());
+
+ $this->assertCaseInfo->processAssert(
+ $this->signifydCases,
+ $this->signifydAddress,
+ $this->signifydData,
+ $this->prices,
+ $this->order->getId(),
+ $this->getCustomerFullName($this->signifydAddress)
+ );
+ }
+
+ /**
+ * Cancel order if test fails, or in the end of variation.
+ *
+ * @return void
+ */
+ public function cleanup()
+ {
+ $this->testStepFactory->create(
+ SignifydCancelOrderStep::class,
+ ['order' => $this->order]
+ )->run();
+ }
+
+ /**
+ * Gets customer full name.
+ *
+ * @param SignifydAddress $billingAddress
+ * @return string
+ */
+ private function getCustomerFullName(SignifydAddress $billingAddress)
+ {
+ return sprintf('%s %s', $billingAddress->getFirstname(), $billingAddress->getLastname());
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydSetWebhookHandlersStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydSetWebhookHandlersStep.php
new file mode 100644
index 0000000000000..2b630b8c1dac8
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/SignifydSetWebhookHandlersStep.php
@@ -0,0 +1,64 @@
+signifydNotifications = $signifydNotifications;
+ $this->signifydData = $signifydData;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ $this->signifydNotifications->open();
+ $this->signifydNotifications->getWebhooksBlock()
+ ->create($this->signifydData->getTeam());
+ }
+
+ /**
+ * Removes webhooks if test fails, or in the end of variation execution.
+ *
+ * @return void
+ */
+ public function cleanup()
+ {
+ $this->signifydNotifications->open();
+ $this->signifydNotifications->getWebhooksBlock()
+ ->cleanup($this->signifydData->getTeam());
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/UnholdAndCancelOrderStep.php b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/UnholdAndCancelOrderStep.php
new file mode 100644
index 0000000000000..0c90b1b76937b
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/TestStep/UnholdAndCancelOrderStep.php
@@ -0,0 +1,84 @@
+placeOrderStatus = $placeOrderStatus;
+ $this->order = $order;
+ $this->testStepFactory = $testStepFactory;
+ }
+
+ /**
+ * Cancel order step.
+ *
+ * If order was held - unhold and then cancel the order.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ if ($this->placeOrderStatus === 'On Hold') {
+ $this->getStepInstance(UnholdOrderStep::class)->run();
+ }
+
+ $this->getStepInstance(CancelOrderStep::class)->run();
+ }
+
+ /**
+ * Creates test step instance with preset params.
+ *
+ * @param string $class
+ * @return TestStepInterface
+ */
+ private function getStepInstance($class)
+ {
+ return $this->testStepFactory->create(
+ $class,
+ ['order' => $this->order]
+ );
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/di.xml
new file mode 100644
index 0000000000000..2ba4b6c97d33f
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/di.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ S1
+
+
+
+
+ S1
+
+
+
+
+ S1
+
+
+
+
+ S1
+
+
+
+
+ S2
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/testcase.xml
new file mode 100644
index 0000000000000..cfbd2e6ace2b4
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Signifyd/Test/etc/testcase.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Block/Adminhtml/CaseInfoTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Block/Adminhtml/CaseInfoTest.php
new file mode 100644
index 0000000000000..71a0d5715d1aa
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Block/Adminhtml/CaseInfoTest.php
@@ -0,0 +1,122 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->order = $this->objectManager->create(Order::class);
+ $this->layout = $this->objectManager->get(LayoutInterface::class);
+ }
+
+ /**
+ * Checks that block has contents when case entity for order is exists
+ * even if Signifyd module is inactive.
+ *
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 0
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ * @magentoAppArea adminhtml
+ */
+ public function testModuleIsInactive()
+ {
+ $this->order->loadByIncrementId('100000001');
+
+ self::assertNotEmpty($this->getBlock()->toHtml());
+ }
+
+ /**
+ * Checks that block does not give contents
+ * if there is no case entity created for order.
+ *
+ * @covers \Magento\Signifyd\Block\Adminhtml\CaseInfo::getCaseEntity
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ * @magentoAppArea adminhtml
+ */
+ public function testCaseEntityNotExists()
+ {
+ $this->order->loadByIncrementId('100000001');
+
+ self::assertEmpty($this->getBlock()->toHtml());
+ }
+
+ /**
+ * Checks that:
+ * - block give contents
+ * - block contents guarantee decision field
+ *
+ * @covers \Magento\Signifyd\Block\Adminhtml\CaseInfo::getScoreClass
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ * @magentoAppArea adminhtml
+ */
+ public function testCaseEntityExists()
+ {
+ $this->order->loadByIncrementId('100000001');
+
+ $block = $this->getBlock();
+ self::assertNotEmpty($block->toHtml());
+ self::assertContains((string) $block->getCaseGuaranteeDisposition(), $block->toHtml());
+ }
+
+ /**
+ * Gets block.
+ *
+ * @return CaseInfo
+ */
+ private function getBlock()
+ {
+ $this->layout->addContainer('order_additional_info', 'Container');
+
+ /** @var CaseInfo $block */
+ $block = $this->layout->addBlock(CaseInfo::class, 'order_case_info', 'order_additional_info');
+ $block->setAttribute('context', $this->getContext());
+ $block->setTemplate('Magento_Signifyd::case_info.phtml');
+
+ return $block;
+ }
+
+ /**
+ * Creates template context with necessary order id param.
+ *
+ * @return Context
+ */
+ private function getContext()
+ {
+ /** @var RequestInterface $request */
+ $request = $this->objectManager->get(RequestInterface::class);
+ $request->setParams(['order_id' => $this->order->getEntityId()]);
+
+ return $this->objectManager->create(Context::class, ['request' => $request]);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Block/FingerprintTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Block/FingerprintTest.php
new file mode 100644
index 0000000000000..1efbb8b6d33e4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Block/FingerprintTest.php
@@ -0,0 +1,63 @@
+loadArea(Area::AREA_FRONTEND);
+
+ $this->objectManager = Bootstrap::getObjectManager();
+ }
+
+ /**
+ * Checks if session id attribute is present when the module is enabled.
+ *
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testSessionIdPresent()
+ {
+ self::assertContains('data-order-session-id', $this->getBlockContents());
+ }
+
+ /**
+ * Checks if block is an empty when the module is disabled.
+ *
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 0
+ */
+ public function testBlockEmpty()
+ {
+ self::assertEmpty($this->getBlockContents());
+ }
+
+ /**
+ * Renders block contents.
+ *
+ * @return string
+ */
+ private function getBlockContents()
+ {
+ $block = $this->objectManager->get(LayoutInterface::class)
+ ->createBlock(Fingerprint::class);
+
+ return $block->fetchView($block->getTemplateFile());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Controller/Webhooks/HandlerTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Controller/Webhooks/HandlerTest.php
new file mode 100644
index 0000000000000..667cb079f9096
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Controller/Webhooks/HandlerTest.php
@@ -0,0 +1,135 @@
+getWebhookRequest();
+ $this->_objectManager->addSharedInstance($webhookRequest, WebhookRequest::class);
+
+ $this->dispatch(self::$entryPoint);
+
+ /** @var CaseRepositoryInterface $caseManagement */
+ $caseRepository = $this->_objectManager->get(CaseRepositoryInterface::class);
+ /** @var CaseInterface $caseEntity */
+ $caseEntity = $caseRepository->getByCaseId($caseId);
+ $orderEntityId = $caseEntity->getOrderId();
+
+ self::assertNotEmpty($caseEntity);
+ self::assertEquals('2017-01-06 12:47:03', $caseEntity->getCreatedAt());
+ self::assertEquals('2017-01-06 12:47:03', $caseEntity->getUpdatedAt());
+ self::assertEquals('Magento', $caseEntity->getAssociatedTeam()['teamName']);
+ self::assertEquals(true, $caseEntity->isGuaranteeEligible());
+ self::assertEquals(CaseInterface::STATUS_OPEN, $caseEntity->getStatus());
+ self::assertEquals($orderEntityId, $caseEntity->getOrderId());
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($caseEntity->getOrderId());
+ $histories = $order->getStatusHistories();
+ self::assertNotEmpty($histories);
+
+ /** @var OrderStatusHistoryInterface $caseCreationComment */
+ $caseComment = array_pop($histories);
+ self::assertInstanceOf(OrderStatusHistoryInterface::class, $caseComment);
+
+ self::assertEquals(
+ "Case Update: New score for the order is 384. Previous score was 553.",
+ $caseComment->getComment()
+ );
+
+ $this->_objectManager->removeSharedInstance(WebhookRequest::class);
+ }
+
+ /**
+ * Tests handling webhook message of cases/test type.
+ * Controller should response with code 200.
+ *
+ * @covers \Magento\Signifyd\Controller\Webhooks\Handler::execute
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testExecuteTestSuccess()
+ {
+ $webhookRequest = $this->getTestWebhookRequest();
+ $this->_objectManager->addSharedInstance($webhookRequest, WebhookRequest::class);
+ $this->dispatch(self::$entryPoint);
+ $this->assertEquals(200, $this->getResponse()->getHttpResponseCode());
+ $this->_objectManager->removeSharedInstance(WebhookRequest::class);
+ }
+
+ /**
+ * Returns mocked WebhookRequest
+ *
+ * @return WebhookRequest|\PHPUnit\Framework\MockObject_MockObject
+ */
+ private function getWebhookRequest()
+ {
+ $webhookRequest = $this->getMockBuilder(WebhookRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookRequest->expects($this->any())
+ ->method('getBody')
+ ->willReturn(file_get_contents(__DIR__ . '/../../_files/webhook_body.json'));
+ $webhookRequest->expects($this->any())
+ ->method('getEventTopic')
+ ->willReturn('cases/rescore');
+ $webhookRequest->expects($this->any())
+ ->method('getHash')
+ ->willReturn('m/X29RcHWPSCDPgQuSXjnyTfKISJDopcdGbVsRLeqy8=');
+
+ return $webhookRequest;
+ }
+
+ /**
+ * Returns mocked test WebhookRequest
+ *
+ * @return WebhookRequest|\PHPUnit\Framework\MockObject_MockObject
+ */
+ private function getTestWebhookRequest()
+ {
+ $webhookRequest = $this->getMockBuilder(WebhookRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $webhookRequest->expects($this->any())
+ ->method('getBody')
+ ->willReturn(file_get_contents(__DIR__ . '/../../_files/webhook_body.json'));
+ $webhookRequest->expects($this->any())
+ ->method('getEventTopic')
+ ->willReturn('cases/test');
+ $webhookRequest->expects($this->any())
+ ->method('getHash')
+ ->willReturn('wyG0r9mOmv1IqVlN6ZqJ5sgA635yKW6lbSsqlYF2b8U=');
+
+ return $webhookRequest;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseManagementTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseManagementTest.php
new file mode 100644
index 0000000000000..30603baf867ff
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseManagementTest.php
@@ -0,0 +1,83 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->caseManagement = $this->objectManager->get(CaseManagement::class);
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseManagement::create
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testCreate()
+ {
+ $order = $this->getOrder();
+ $case = $this->caseManagement->create($order->getEntityId());
+
+ self::assertNotEmpty($case->getEntityId());
+ self::assertEquals(CaseInterface::STATUS_PENDING, $case->getStatus());
+ self::assertEquals(CaseInterface::GUARANTEE_PENDING, $case->getGuaranteeDisposition());
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseManagement::getByOrderId
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ */
+ public function testGetByOrderId()
+ {
+ $order = $this->getOrder();
+ $case = $this->caseManagement->getByOrderId($order->getEntityId());
+
+ self::assertEquals(CaseInterface::STATUS_PROCESSING, $case->getStatus());
+ self::assertEquals(CaseInterface::DISPOSITION_GOOD, $case->getReviewDisposition());
+ self::assertEquals('2016-12-12 15:17:17', $case->getCreatedAt());
+ self::assertEquals('2016-12-12 19:23:16', $case->getUpdatedAt());
+ }
+
+ /**
+ * Get stored order
+ * @return OrderInterface
+ */
+ private function getOrder()
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, '100000001')
+ ->create();
+
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $orders = $orderRepository->getList($searchCriteria)
+ ->getItems();
+
+ /** @var OrderInterface $order */
+ return array_pop($orders);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseRepositoryTest.php
new file mode 100644
index 0000000000000..ca98a20b15bec
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseRepositoryTest.php
@@ -0,0 +1,148 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->filterBuilder = $this->objectManager->get(FilterBuilder::class);
+ $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $this->repository = $this->objectManager->get(CaseRepository::class);
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseRepository::delete
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ */
+ public function testDelete()
+ {
+ $filters = [
+ $this->filterBuilder->setField('case_id')
+ ->setValue(123)
+ ->create()
+ ];
+ $searchCriteria = $this->searchCriteriaBuilder->addFilters($filters)->create();
+ $cases = $this->repository->getList($searchCriteria)
+ ->getItems();
+
+ $case = array_pop($cases);
+ $this->repository->delete($case);
+
+ self::assertEmpty($this->repository->getList($searchCriteria)->getItems());
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseRepository::getById
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ */
+ public function testGetById()
+ {
+ $filters = [
+ $this->filterBuilder->setField('case_id')
+ ->setValue(123)
+ ->create()
+ ];
+ $searchCriteria = $this->searchCriteriaBuilder->addFilters($filters)->create();
+ $cases = $this->repository->getList($searchCriteria)
+ ->getItems();
+
+ $case = array_pop($cases);
+
+ $found = $this->repository->getById($case->getEntityId());
+
+ self::assertNotEmpty($found->getEntityId());
+ self::assertEquals($case->getEntityId(), $found->getEntityId());
+ self::assertEquals($case->getOrderId(), $found->getOrderId());
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseRepository::getList
+ * @magentoDataFixture Magento/Signifyd/_files/multiple_cases.php
+ */
+ public function testGetListDateInterval()
+ {
+ $startDateInterval = [
+ $this->filterBuilder->setField('created_at')
+ ->setConditionType('gteq')
+ ->setValue('2016-12-01 00:00:01')
+ ->create()
+ ];
+ $endDateInterval = [
+ $this->filterBuilder->setField('created_at')
+ ->setConditionType('lteq')
+ ->setValue('2016-12-03 23:59:59')
+ ->create()
+ ];
+
+ $this->searchCriteriaBuilder->addFilters($startDateInterval);
+ $searchCriteria = $this->searchCriteriaBuilder->addFilters($endDateInterval)->create();
+ $items = $this->repository->getList($searchCriteria)
+ ->getItems();
+
+ self::assertCount(3, $items);
+
+ for ($i = 1; $i < 4; $i ++) {
+ $current = array_shift($items);
+ self::assertEquals($i, $current->getCaseId());
+ }
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseRepository::getList
+ * @magentoDataFixture Magento/Signifyd/_files/multiple_cases.php
+ */
+ public function testGetListStatusProcessing()
+ {
+ $filters = [
+ $this->filterBuilder->setField('status')
+ ->setValue(CaseInterface::STATUS_PROCESSING)
+ ->create()
+ ];
+
+ $searchCriteria = $this->searchCriteriaBuilder->addFilters($filters)->create();
+ $items = $this->repository->getList($searchCriteria)
+ ->getItems();
+
+ self::assertCount(1, $items);
+
+ $case = array_pop($items);
+ self::assertEquals(123, $case->getCaseId());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/CreationServiceTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/CreationServiceTest.php
new file mode 100644
index 0000000000000..6cf1e0bfddc9a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/CreationServiceTest.php
@@ -0,0 +1,245 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->requestBuilder = $this->getMockBuilder(RequestBuilder::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['doRequest'])
+ ->getMock();
+
+ $apiClient = $this->objectManager->create(
+ ApiClient::class,
+ ['requestBuilder' => $this->requestBuilder]
+ );
+
+ $gateway = $this->objectManager->create(
+ Gateway::class,
+ ['apiClient' => $apiClient]
+ );
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['error'])
+ ->getMockForAbstractClass();
+
+ $this->service = $this->objectManager->create(
+ CreationService::class,
+ [
+ 'signifydGateway' => $gateway,
+ 'logger' => $this->logger
+ ]
+ );
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseServices\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testCreateForOrderWithEmptyResponse()
+ {
+ $order = $this->getOrder();
+ $exceptionMessage = 'Response is not valid JSON: Decoding failed: Syntax error';
+
+ $this->requestBuilder->expects(self::once())
+ ->method('doRequest')
+ ->willThrowException(new ApiCallException($exceptionMessage));
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with($exceptionMessage);
+
+ $result = $this->service->createForOrder($order->getEntityId());
+ self::assertTrue($result);
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseServices\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testCreateForOrderWithBadResponse()
+ {
+ $order = $this->getOrder();
+ $responseData = [
+ 'messages' => [
+ 'Something wrong'
+ ]
+ ];
+ $exceptionMessage = 'Bad Request - The request could not be parsed. Response: ' . json_encode($responseData);
+
+ $this->requestBuilder->expects(self::once())
+ ->method('doRequest')
+ ->willThrowException(new ApiCallException($exceptionMessage));
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with($exceptionMessage);
+
+ $result = $this->service->createForOrder($order->getEntityId());
+ self::assertTrue($result);
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseServices\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testCreateOrderWithEmptyInvestigationId()
+ {
+ $order = $this->getOrder();
+
+ $this->requestBuilder->expects(self::once())
+ ->method('doRequest')
+ ->willReturn([]);
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with('Expected field "investigationId" missed.');
+
+ $result = $this->service->createForOrder($order->getEntityId());
+ self::assertTrue($result);
+ }
+
+ /**
+ * @covers \Magento\Signifyd\Model\CaseServices\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testCreateForOrder()
+ {
+ $order = $this->getOrder();
+
+ $this->requestBuilder->expects(self::once())
+ ->method('doRequest')
+ ->willReturn(['investigationId' => 123123]);
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $result = $this->service->createForOrder($order->getEntityId());
+ self::assertTrue($result);
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId(123123);
+ $gridGuarantyStatus = $this->getOrderGridGuarantyStatus($caseEntity->getOrderId());
+
+ self::assertNotEmpty($caseEntity);
+ self::assertEquals($order->getEntityId(), $caseEntity->getOrderId());
+ self::assertEquals(
+ $gridGuarantyStatus,
+ $caseEntity->getGuaranteeDisposition(),
+ 'Signifyd guaranty status in sales_order_grid table does not match case entity guaranty status'
+ );
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($caseEntity->getOrderId());
+ self::assertEquals(Order::STATE_HOLDED, $order->getState());
+
+ $histories = $order->getStatusHistories();
+ self::assertNotEmpty($histories);
+
+ /** @var OrderStatusHistoryInterface $orderHoldComment */
+ $orderHoldComment = array_pop($histories);
+ self::assertInstanceOf(OrderStatusHistoryInterface::class, $orderHoldComment);
+ self::assertEquals("Awaiting the Signifyd guarantee disposition.", $orderHoldComment->getComment());
+ }
+
+ /**
+ * Get stored order
+ *
+ * @return OrderInterface
+ */
+ private function getOrder()
+ {
+ if ($this->order === null) {
+
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, '100000001')
+ ->create();
+
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $orders = $orderRepository->getList($searchCriteria)
+ ->getItems();
+
+ $this->order = array_pop($orders);
+ }
+
+ return $this->order;
+ }
+
+ /**
+ * Returns value of signifyd_guarantee_status column from sales order grid
+ *
+ * @param int $orderEntityId
+ * @return string|null
+ */
+ private function getOrderGridGuarantyStatus($orderEntityId)
+ {
+ /** @var Collection $orderGridCollection */
+ $orderGridCollection = $this->objectManager->get(Collection::class);
+
+ $items = $orderGridCollection->addFilter($orderGridCollection->getIdFieldName(), $orderEntityId)
+ ->getItems();
+ $result = array_pop($items);
+
+ return isset($result['signifyd_guarantee_status']) ? $result['signifyd_guarantee_status'] : null;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/UpdatingServiceTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/UpdatingServiceTest.php
new file mode 100644
index 0000000000000..50e510ca072be
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/CaseServices/UpdatingServiceTest.php
@@ -0,0 +1,186 @@
+objectManager = Bootstrap::getObjectManager();
+
+ /** @var GeneratorFactory $messageFactory */
+ $messageFactory = $this->objectManager->get(GeneratorFactory::class);
+ $messageGenerator = $messageFactory->create('cases/creation');
+
+ $this->service = $this->objectManager->create(UpdatingService::class, [
+ 'messageGenerator' => $messageGenerator
+ ]);
+ }
+
+ /**
+ * Checks case updating flow and messages in order comments history.
+ * Also checks that order is unholded when case guarantee disposition is APPROVED.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ */
+ public function testUpdate()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'score' => 750,
+ 'orderId' => '100000001',
+ 'reviewDisposition' => CaseInterface::DISPOSITION_FRAUDULENT,
+ 'associatedTeam' => [
+ 'teamName' => 'AnyTeam',
+ 'teamId' => 26,
+ 'getAutoDismiss' => true,
+ 'getTeamDismissalDays' => 2
+ ],
+ 'createdAt' => '2017-01-05T14:23:26-0800',
+ 'updatedAt' => '2017-01-05T14:44:26-0800',
+ 'guaranteeDisposition' => CaseInterface::GUARANTEE_APPROVED
+ ];
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ /** @var CaseInterface $caseEntity */
+ $caseEntity = $caseRepository->getByCaseId($caseId);
+
+ $this->service->update($caseEntity, $data);
+
+ $caseEntity = $caseRepository->getByCaseId($caseId);
+ $orderEntityId = $caseEntity->getOrderId();
+ $gridGuarantyStatus = $this->getOrderGridGuarantyStatus($orderEntityId);
+
+ self::assertNotEmpty($caseEntity);
+ self::assertEquals('2017-01-05 22:23:26', $caseEntity->getCreatedAt());
+ self::assertEquals(CaseInterface::GUARANTEE_APPROVED, $caseEntity->getGuaranteeDisposition());
+ self::assertEquals('AnyTeam', $caseEntity->getAssociatedTeam()['teamName']);
+ self::assertEquals(true, $caseEntity->isGuaranteeEligible());
+ self::assertEquals(CaseInterface::STATUS_PROCESSING, $caseEntity->getStatus());
+ self::assertEquals($orderEntityId, $caseEntity->getOrderId());
+ self::assertEquals(
+ $gridGuarantyStatus,
+ $caseEntity->getGuaranteeDisposition(),
+ 'Signifyd guaranty status in sales_order_grid table does not match case entity guaranty status'
+ );
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($caseEntity->getOrderId());
+ self::assertEquals(Order::STATE_PROCESSING, $order->getState());
+ $histories = $order->getStatusHistories();
+ self::assertNotEmpty($histories);
+
+ /** @var OrderStatusHistoryInterface $caseCreationComment */
+ $caseCreationComment = array_pop($histories);
+ self::assertInstanceOf(OrderStatusHistoryInterface::class, $caseCreationComment);
+ self::assertEquals("Signifyd Case $caseId has been created for order.", $caseCreationComment->getComment());
+ }
+
+ /**
+ * Checks that order is holded when case guarantee disposition is DECLINED.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @magentoDataFixture Magento/Signifyd/_files/approved_case.php
+ */
+ public function testOrderStateAfterDeclinedGuaranteeDisposition()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'orderId' => '100000001',
+ 'guaranteeDisposition' => CaseInterface::GUARANTEE_DECLINED
+ ];
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId($caseId);
+
+ $this->service->update($caseEntity, $data);
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($caseEntity->getOrderId());
+
+ self::assertEquals(Order::STATE_HOLDED, $order->getState());
+ }
+
+ /**
+ * Checks that order doesn't become holded
+ * when previous case guarantee disposition was DECLINED
+ * and webhook without guarantee disposition was received.
+ *
+ * @covers \Magento\Signifyd\Model\CaseServices\UpdatingService::update
+ * @magentoDataFixture Magento/Signifyd/_files/declined_case.php
+ */
+ public function testOrderStateAfterWebhookWithoutGuaranteeDisposition()
+ {
+ $caseId = 123;
+ $data = [
+ 'caseId' => $caseId,
+ 'orderId' => '100000001'
+ ];
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId($caseId);
+
+ $this->service->update($caseEntity, $data);
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($caseEntity->getOrderId());
+
+ self::assertEquals(Order::STATE_PROCESSING, $order->getState());
+ }
+
+ /**
+ * Returns value of signifyd_guarantee_status column from sales order grid
+ *
+ * @param int $orderEntityId
+ * @return string|null
+ */
+ private function getOrderGridGuarantyStatus($orderEntityId)
+ {
+ /** @var Collection $orderGridCollection */
+ $orderGridCollection = $this->objectManager->get(Collection::class);
+
+ $items = $orderGridCollection->addFilter($orderGridCollection->getIdFieldName(), $orderEntityId)
+ ->getItems();
+ $result = array_pop($items);
+
+ return isset($result['signifyd_guarantee_status']) ? $result['signifyd_guarantee_status'] : null;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CancelingServiceTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CancelingServiceTest.php
new file mode 100644
index 0000000000000..2cc7a9a1f240a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CancelingServiceTest.php
@@ -0,0 +1,158 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->gateway = $this->getMockBuilder(Gateway::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['cancelGuarantee'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->service = $this->objectManager->create(CancelingService::class, [
+ 'gateway' => $this->gateway,
+ 'logger' => $this->logger
+ ]);
+ }
+
+ /**
+ * Checks a test case, when Signifyd guarantee was canceled.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testCancelForOrderWithCanceledGuarantee()
+ {
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId(self::$caseId);
+ $caseEntity->setGuaranteeDisposition(CaseInterface::GUARANTEE_CANCELED);
+ $caseRepository->save($caseEntity);
+
+ $this->gateway->expects(self::never())
+ ->method('cancelGuarantee');
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $result = $this->service->cancelForOrder($caseEntity->getOrderId());
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when Signifyd gateway throws an exception.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/approved_case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testCancelForOrderWithFailedRequest()
+ {
+ $exceptionMessage = 'Something wrong.';
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId(self::$caseId);
+
+ $this->gateway->expects(self::once())
+ ->method('cancelGuarantee')
+ ->with(self::equalTo(self::$caseId))
+ ->willThrowException(new GatewayException($exceptionMessage));
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with(self::equalTo($exceptionMessage));
+
+ $result = $this->service->cancelForOrder($caseEntity->getOrderId());
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when request to cancel is submitted and case entity is updated successfully.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CancelingService::cancelForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/approved_case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testCancelForOrder()
+ {
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $caseEntity = $caseRepository->getByCaseId(self::$caseId);
+
+ $this->gateway->expects(self::once())
+ ->method('cancelGuarantee')
+ ->with(self::equalTo(self::$caseId))
+ ->willReturn(CaseInterface::GUARANTEE_CANCELED);
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $result = $this->service->cancelForOrder($caseEntity->getOrderId());
+ self::assertTrue($result);
+
+ $updatedCase = $caseRepository->getByCaseId(self::$caseId);
+ self::assertEquals(CaseInterface::GUARANTEE_CANCELED, $updatedCase->getGuaranteeDisposition());
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($updatedCase->getOrderId());
+ $histories = $order->getStatusHistories();
+ self::assertNotEmpty($histories);
+
+ /** @var OrderStatusHistoryInterface $caseCreationComment */
+ $caseCreationComment = array_pop($histories);
+ self::assertInstanceOf(OrderStatusHistoryInterface::class, $caseCreationComment);
+ self::assertEquals('Case Update: Case guarantee has been cancelled.', $caseCreationComment->getComment());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CreationServiceTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CreationServiceTest.php
new file mode 100644
index 0000000000000..157e3270648b3
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/Guarantee/CreationServiceTest.php
@@ -0,0 +1,155 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->gateway = $this->getMockBuilder(Gateway::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['submitCaseForGuarantee'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->service = $this->objectManager->create(CreationService::class, [
+ 'gateway' => $this->gateway,
+ 'logger' => $this->logger
+ ]);
+ }
+
+ /**
+ * Checks a test case, when Signifyd case entity cannot be found
+ * for a specified order.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CreationService::createForOrder
+ */
+ public function testCreateWithoutCaseEntity()
+ {
+ $orderId = 123;
+
+ $this->gateway->expects(self::never())
+ ->method('submitCaseForGuarantee');
+
+ $result = $this->service->createForOrder($orderId);
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when request is failing.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ */
+ public function testCreateWithFailedRequest()
+ {
+ $caseEntity = $this->getCaseEntity();
+
+ $this->gateway->expects(self::once())
+ ->method('submitCaseForGuarantee')
+ ->willThrowException(new GatewayException('Something wrong'));
+
+ $this->logger->expects(self::once())
+ ->method('error')
+ ->with('Something wrong');
+
+ $result = $this->service->createForOrder($caseEntity->getOrderId());
+ self::assertFalse($result);
+ }
+
+ /**
+ * Checks a test case, when case entity is updated successfully.
+ *
+ * @covers \Magento\Signifyd\Model\Guarantee\CreationService::createForOrder
+ * @magentoDataFixture Magento/Signifyd/_files/case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testCreate()
+ {
+ $caseEntity = $this->getCaseEntity();
+
+ $this->gateway->expects(self::once())
+ ->method('submitCaseForGuarantee')
+ ->with($caseEntity->getCaseId())
+ ->willReturn(CaseInterface::GUARANTEE_IN_REVIEW);
+
+ $this->logger->expects(self::never())
+ ->method('error');
+
+ $result = $this->service->createForOrder($caseEntity->getOrderId());
+ self::assertTrue($result);
+
+ $updatedCase = $this->getCaseEntity();
+ self::assertEquals(CaseInterface::GUARANTEE_IN_REVIEW, $updatedCase->getGuaranteeDisposition());
+ self::assertEquals(CaseInterface::STATUS_PROCESSING, $updatedCase->getStatus());
+
+ /** @var OrderRepositoryInterface $orderRepository */
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $order = $orderRepository->get($updatedCase->getOrderId());
+ $histories = $order->getStatusHistories();
+ self::assertNotEmpty($histories);
+
+ /** @var OrderStatusHistoryInterface $caseCreationComment */
+ $caseCreationComment = array_pop($histories);
+ self::assertInstanceOf(OrderStatusHistoryInterface::class, $caseCreationComment);
+ self::assertEquals('Case Update: Case is submitted for guarantee.', $caseCreationComment->getComment());
+ }
+
+ /**
+ * Gets case entity.
+ *
+ * @return \Magento\Signifyd\Api\Data\CaseInterface|null
+ */
+ private function getCaseEntity()
+ {
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ return $caseRepository->getByCaseId(123);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderTest.php
new file mode 100644
index 0000000000000..7e7e20c873948
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Model/SignifydGateway/Request/CreateCaseBuilderTest.php
@@ -0,0 +1,292 @@
+loadArea(Area::AREA_FRONTEND);
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->dateTimeFactory = $this->objectManager->create(DateTimeFactory::class);
+ $this->caseBuilder = $this->objectManager->create(CreateCaseBuilder::class);
+ }
+
+ /**
+ * Test builder on order with customer, simple product, frontend area,
+ * PayPal gateway, shipping and billing addresses, with two orders
+ *
+ * @covers \Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilder::build()
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function testCreateCaseBuilderWithFullSetOfData()
+ {
+ /** @var Order $order */
+ $order = $this->objectManager->create(Order::class);
+ $order->loadByIncrementId('100000001');
+
+ $orderItems = $order->getAllItems();
+ $product = $orderItems[0]->getProduct();
+ $payment = $order->getPayment();
+ $billingAddress = $order->getBillingAddress();
+ $shippingAddress = $order->getShippingAddress();
+
+ /** @var CustomerRepositoryInterface $customerRepository */
+ $customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class);
+ $customer = $customerRepository->getById($order->getCustomerId());
+
+ $productMetadata = $this->objectManager->create(ProductMetadataInterface::class);
+
+ /** @var SignifydOrderSessionId $signifydOrderSessionId */
+ $signifydOrderSessionId = $this->objectManager->create(SignifydOrderSessionId::class);
+
+ $expected = [
+ 'purchase' => [
+ 'orderSessionId' => $signifydOrderSessionId->get($order->getQuoteId()),
+ 'browserIpAddress' => $order->getRemoteIp(),
+ 'orderId' => $order->getIncrementId(),
+ 'createdAt' => date('c', strtotime(date('Y-m-d 00:00:55'))),
+ 'paymentGateway' => 'paypal_account',
+ 'transactionId' => $payment->getLastTransId(),
+ 'currency' => $order->getOrderCurrencyCode(),
+ 'avsResponseCode' => 'U',
+ 'cvvResponseCode' => '',
+ 'orderChannel' => 'WEB',
+ 'totalPrice' => $order->getGrandTotal(),
+ 'shipments' => [
+ 0 => [
+ 'shipper' => 'Flat Rate',
+ 'shippingMethod' => 'Fixed',
+ 'shippingPrice' => $order->getShippingAmount()
+ ]
+ ],
+ 'products' => [
+ 0 => [
+ 'itemId' => $orderItems[0]->getSku(),
+ 'itemName' => $orderItems[0]->getName(),
+ 'itemPrice' => $orderItems[0]->getPrice(),
+ 'itemQuantity' => $orderItems[0]->getQtyOrdered(),
+ 'itemUrl' => $product->getProductUrl(),
+ 'itemWeight' => $product->getWeight()
+ ],
+ 1 => [
+ 'itemId' => $orderItems[1]->getSku(),
+ 'itemName' => $orderItems[1]->getName(),
+ 'itemPrice' => $orderItems[1]->getPrice(),
+ 'itemQuantity' => $orderItems[1]->getQtyOrdered(),
+ 'itemUrl' => $product->getProductUrl(),
+ 'itemWeight' => $product->getWeight()
+ ]
+ ],
+ 'paymentMethod' => 'PAYPAL_ACCOUNT'
+ ],
+ 'card' => [
+ 'cardHolderName' => 'firstname lastname',
+ 'last4' => $payment->getCcLast4(),
+ 'expiryMonth' => $payment->getCcExpMonth(),
+ 'expiryYear' => $payment->getCcExpYear(),
+ 'billingAddress' => [
+ 'streetAddress' => 'street',
+ 'city' => $billingAddress->getCity(),
+ 'provinceCode' => $billingAddress->getRegionCode(),
+ 'postalCode' => $billingAddress->getPostcode(),
+ 'countryCode' => $billingAddress->getCountryId(),
+ 'unit' => ''
+ ]
+ ],
+ 'recipient' => [
+ 'fullName' => $shippingAddress->getName(),
+ 'confirmationEmail' => $shippingAddress->getEmail(),
+ 'confirmationPhone' => $shippingAddress->getTelephone(),
+ 'deliveryAddress' => [
+ 'streetAddress' => '6161 West Centinela Avenue',
+ 'unit' => 'app. 33',
+ 'city' => $shippingAddress->getCity(),
+ 'provinceCode' => $shippingAddress->getRegionCode(),
+ 'postalCode' => $shippingAddress->getPostcode(),
+ 'countryCode' => $shippingAddress->getCountryId()
+ ]
+ ],
+ 'userAccount' => [
+ 'email' => $customer->getEmail(),
+ 'username' => $customer->getEmail(),
+ 'phone' => $order->getBillingAddress()->getTelephone(),
+ 'accountNumber' => $customer->getId(),
+ 'createdDate' => $this->formatDate($customer->getCreatedAt()),
+ 'lastUpdateDate' => $this->formatDate($customer->getUpdatedAt()),
+ 'aggregateOrderCount' => 2,
+ 'aggregateOrderDollars' => 150.0
+ ],
+ 'seller' => $this->getSellerData(),
+ 'platformAndClient' => [
+ 'storePlatform' => $productMetadata->getName() . ' ' . $productMetadata->getEdition(),
+ 'storePlatformVersion' => $productMetadata->getVersion(),
+ 'signifydClientApp' => $productMetadata->getName(),
+ 'signifydClientAppVersion' => '1.0'
+ ]
+ ];
+
+ self::assertEquals(
+ $expected,
+ $this->caseBuilder->build($order->getEntityId())
+ );
+ }
+
+ /**
+ * Test builder on order with guest, virtual product, admin area,
+ * none PayPal gateway, no shipping address, without credit card data
+ *
+ * @covers \Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilder::build()
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_guest_and_virtual_product.php
+ */
+ public function testCreateCaseBuilderWithVirtualProductAndGuest()
+ {
+ /** @var Order $order */
+ $order = $this->objectManager->create(Order::class);
+ $order->loadByIncrementId('100000002');
+
+ $scope = $this->objectManager->get(ScopeInterface::class);
+ $scope->setCurrentScope(Area::AREA_ADMINHTML);
+
+ $orderItems = $order->getAllItems();
+ $product = $orderItems[0]->getProduct();
+ $payment = $order->getPayment();
+ $billingAddress = $order->getBillingAddress();
+ $productMetadata = $this->objectManager->create(ProductMetadataInterface::class);
+
+ /** @var SignifydOrderSessionId $quoteSessionId */
+ $quoteSessionId = $this->objectManager->create(SignifydOrderSessionId::class);
+
+ $expected = [
+ 'purchase' => [
+ 'orderSessionId' => $quoteSessionId->get($order->getQuoteId()),
+ 'browserIpAddress' => $order->getRemoteIp(),
+ 'orderId' => $order->getIncrementId(),
+ 'createdAt' => '2016-12-12T12:00:55+00:00',
+ 'paymentGateway' => $payment->getMethod(),
+ 'transactionId' => $payment->getLastTransId(),
+ 'currency' => $order->getOrderCurrencyCode(),
+ 'avsResponseCode' => 'Y',
+ 'cvvResponseCode' => 'M',
+ 'orderChannel' => 'PHONE',
+ 'totalPrice' => $order->getGrandTotal(),
+ 'products' => [
+ 0 => [
+ 'itemId' => $orderItems[0]->getSku(),
+ 'itemName' => $orderItems[0]->getName(),
+ 'itemPrice' => $orderItems[0]->getPrice(),
+ 'itemQuantity' => $orderItems[0]->getQtyOrdered(),
+ 'itemUrl' => $product->getProductUrl()
+ ],
+ ],
+ 'paymentMethod' => 'PAYMENT_CARD'
+ ],
+ 'card' => [
+ 'cardHolderName' => 'firstname lastname',
+ 'billingAddress' => [
+ 'streetAddress' => 'street',
+ 'city' => $billingAddress->getCity(),
+ 'provinceCode' => $billingAddress->getRegionCode(),
+ 'postalCode' => $billingAddress->getPostcode(),
+ 'countryCode' => $billingAddress->getCountryId(),
+ 'unit' => ''
+ ]
+ ],
+ 'seller' => $this->getSellerData(),
+ 'platformAndClient' => [
+ 'storePlatform' => $productMetadata->getName() . ' ' . $productMetadata->getEdition(),
+ 'storePlatformVersion' => $productMetadata->getVersion(),
+ 'signifydClientApp' => $productMetadata->getName(),
+ 'signifydClientAppVersion' => '1.0'
+ ]
+ ];
+
+ self::assertEquals(
+ $expected,
+ $this->caseBuilder->build($order->getEntityId())
+ );
+ }
+
+ /**
+ * Return seller data according to fixture
+ *
+ * @return array
+ */
+ private function getSellerData()
+ {
+ return [
+ 'name' => 'Sample Store',
+ 'domain' => 'm2.com',
+ 'shipFromAddress' => [
+ 'streetAddress' => '6161 West Centinela Avenue',
+ 'unit' => 'app. 111',
+ 'city' => 'Culver City',
+ 'provinceCode' => 'AE',
+ 'postalCode' => '90230',
+ 'countryCode' => 'US',
+ ],
+ 'corporateAddress' => [
+ 'streetAddress' => '5th Avenue',
+ 'unit' => '75',
+ 'city' => 'New York',
+ 'provinceCode' => 'MH',
+ 'postalCode' => '19032',
+ 'countryCode' => 'US',
+ ],
+ ];
+ }
+
+ /**
+ * Format date in ISO8601
+ *
+ * @param string $date
+ * @return string
+ */
+ private function formatDate($date)
+ {
+ $result = $this->dateTimeFactory->create(
+ $date,
+ new \DateTimeZone('UTC')
+ );
+
+ return $result->format(\DateTime::ATOM);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php
new file mode 100644
index 0000000000000..d4204314453e5
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Observer/PlaceOrderTest.php
@@ -0,0 +1,172 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->creationService = $this->getMockBuilder(CaseCreationServiceInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['createForOrder'])
+ ->getMock();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->placeOrder = $this->objectManager->create(PlaceOrder::class, [
+ 'caseCreationService' => $this->creationService,
+ 'logger' => $this->logger
+ ]);
+ }
+
+ /**
+ * Checks a case when order placed with offline payment method.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testExecuteWithOfflinePayment()
+ {
+ $order = $this->getOrder('100000005');
+ $this->creationService->expects(self::never())
+ ->method('createForOrder');
+
+ $event = $this->objectManager->create(
+ Event::class,
+ [
+ 'data' => ['order' => $order]
+ ]
+ );
+
+ /** @var Observer $observer */
+ $observer = $this->objectManager->get(Observer::class);
+ $observer->setEvent($event);
+
+ $this->placeOrder->execute($observer);
+ }
+
+ /**
+ * Checks a test case when order placed with online payment method.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testExecute()
+ {
+ $order = $this->getOrder('100000001');
+
+ $this->creationService->expects(self::once())
+ ->method('createForOrder')
+ ->with(self::equalTo($order->getEntityId()));
+
+ $event = $this->objectManager->create(
+ Event::class,
+ [
+ 'data' => ['order' => $order]
+ ]
+ );
+
+ /** @var Observer $observer */
+ $observer = $this->objectManager->get(Observer::class);
+ $observer->setEvent($event);
+
+ $this->placeOrder->execute($observer);
+ }
+
+ /**
+ * Checks a test case when observer event contains two orders:
+ * one order with offline payment and one order with online payment.
+ *
+ * @covers \Magento\Signifyd\Observer\PlaceOrder::execute
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ * @magentoDataFixture Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
+ */
+ public function testExecuteWithMultipleOrders()
+ {
+ $orderWithOnlinePayment = $this->getOrder('100000001');
+ $orderWithOfflinePayment = $this->getOrder('100000005');
+
+ // this service mock should be called only once for the order with online payment method.
+ $this->creationService->expects(self::once())
+ ->method('createForOrder')
+ ->with(self::equalTo($orderWithOnlinePayment->getEntityId()));
+
+ $event = $this->objectManager->create(
+ Event::class,
+ [
+ 'data' => ['orders' => [$orderWithOfflinePayment, $orderWithOnlinePayment]]
+ ]
+ );
+
+ /** @var Observer $observer */
+ $observer = $this->objectManager->get(Observer::class);
+ $observer->setEvent($event);
+
+ $this->placeOrder->execute($observer);
+ }
+
+ /**
+ * Gets stored order.
+ *
+ * @param string $incrementId
+ * @return OrderInterface
+ */
+ private function getOrder($incrementId)
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId)
+ ->create();
+
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $orders = $orderRepository->getList($searchCriteria)
+ ->getItems();
+
+ $order = array_pop($orders);
+
+ return $order;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/CancelOrderTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/CancelOrderTest.php
new file mode 100644
index 0000000000000..7c1af95bdb89c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/CancelOrderTest.php
@@ -0,0 +1,114 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->apiClient = $this->getMockBuilder(ApiClient::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['makeApiCall'])
+ ->getMock();
+
+ $this->objectManager->addSharedInstance($this->apiClient, ApiClient::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown()
+ {
+ $this->objectManager->removeSharedInstance(ApiClient::class);
+ }
+
+ /**
+ * Checks a test case, when order has been cancelled
+ * and calls plugin to cancel Signifyd case guarantee.
+ *
+ * @covers \Magento\Signifyd\Plugin\OrderPlugin::afterCancel
+ * @magentoDataFixture Magento/Signifyd/_files/approved_case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testAfterCancel()
+ {
+ $order = $this->getOrder();
+
+ $this->apiClient->expects(self::once())
+ ->method('makeApiCall')
+ ->with(
+ self::equalTo('/cases/' . self::$caseId . '/guarantee'),
+ 'PUT',
+ [
+ 'guaranteeDisposition' => CaseInterface::GUARANTEE_CANCELED
+ ]
+ )
+ ->willReturn([
+ 'disposition' => CaseInterface::GUARANTEE_CANCELED
+ ]);
+
+ /** @var OrderManagementInterface $orderService */
+ $orderService = $this->objectManager->get(OrderManagementInterface::class);
+ $orderService->cancel($order->getEntityId());
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $case = $caseRepository->getByCaseId(self::$caseId);
+
+ self::assertEquals(CaseInterface::GUARANTEE_CANCELED, $case->getGuaranteeDisposition());
+ }
+
+ /**
+ * Get stored order.
+ *
+ * @return OrderInterface
+ */
+ private function getOrder()
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, '100000001')
+ ->create();
+
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $orders = $orderRepository->getList($searchCriteria)
+ ->getItems();
+
+ /** @var OrderInterface $order */
+ return array_pop($orders);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/DenyPaymentTest.php b/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/DenyPaymentTest.php
new file mode 100644
index 0000000000000..72da71a630dff
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/Plugin/DenyPaymentTest.php
@@ -0,0 +1,209 @@
+objectManager = Bootstrap::getObjectManager();
+
+ $this->apiClient = $this->getMockBuilder(ApiClient::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['makeApiCall'])
+ ->getMock();
+
+ $this->registry = $this->objectManager->get(Registry::class);
+
+ $this->objectManager->addSharedInstance($this->apiClient, ApiClient::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown()
+ {
+ $this->objectManager->removeSharedInstance(ApiClient::class);
+ }
+
+ /**
+ * Checks a test case, when payment has been denied
+ * and calls plugin to cancel Signifyd case guarantee.
+ *
+ * @covers \Magento\Signifyd\Plugin\PaymentPlugin::afterDenyPayment
+ * @magentoDataFixture Magento/Signifyd/_files/approved_case.php
+ * @magentoConfigFixture current_store fraud_protection/signifyd/active 1
+ */
+ public function testAfterDenyPayment()
+ {
+ $order = $this->getOrder();
+ $this->registry->register('current_order', $order);
+
+ $this->apiClient->expects(self::once())
+ ->method('makeApiCall')
+ ->with(
+ self::equalTo('/cases/' . self::$caseId . '/guarantee'),
+ 'PUT',
+ [
+ 'guaranteeDisposition' => CaseInterface::GUARANTEE_CANCELED
+ ]
+ )
+ ->willReturn([
+ 'disposition' => CaseInterface::GUARANTEE_CANCELED
+ ]);
+
+ /** @var \Magento\Sales\Model\Order\Payment $payment */
+ $payment = $order->getPayment();
+ $payment->setData('method_instance', $this->getMethodInstance());
+ $payment->deny();
+
+ /** @var CaseRepositoryInterface $caseRepository */
+ $caseRepository = $this->objectManager->get(CaseRepositoryInterface::class);
+ $case = $caseRepository->getByCaseId(self::$caseId);
+
+ self::assertEquals(CaseInterface::GUARANTEE_CANCELED, $case->getGuaranteeDisposition());
+ }
+
+ /**
+ * Get stored order.
+ *
+ * @return OrderInterface
+ */
+ private function getOrder()
+ {
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, '100000001')
+ ->create();
+
+ $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class);
+ $orders = $orderRepository->getList($searchCriteria)
+ ->getItems();
+
+ /** @var OrderInterface $order */
+ return array_pop($orders);
+ }
+
+ /**
+ * Gets payment method instance.
+ *
+ * @return Express
+ */
+ private function getMethodInstance()
+ {
+ /** @var PaymentInfo $infoInstance */
+ $infoInstance = $this->objectManager->get(PaymentInfo::class);
+ $infoInstance->setAdditionalInformation(
+ Info::PAYMENT_STATUS_GLOBAL,
+ Info::PAYMENTSTATUS_PENDING
+ );
+ $infoInstance->setAdditionalInformation(
+ Info::PENDING_REASON_GLOBAL,
+ Info::PAYMENTSTATUS_PENDING
+ );
+
+ /** @var Express $methodInstance */
+ $methodInstance = $this->objectManager->create(
+ Express::class,
+ ['proFactory' => $this->getProFactory()]
+ );
+ $methodInstance->setData('info_instance', $infoInstance);
+
+ return $methodInstance;
+ }
+
+ /**
+ * Gets Pro factory mock.
+ *
+ * @return ProFactory|MockObject
+ */
+ protected function getProFactory()
+ {
+ $pro = $this->getMockBuilder(Pro::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getApi', 'setMethod', 'getConfig', '__wakeup', 'reviewPayment'])
+ ->getMock();
+ $nvpClient = $this->getMockBuilder(Nvp::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $pro->method('getConfig')
+ ->willReturn($this->getConfig());
+ $pro->method('getApi')
+ ->willReturn($nvpClient);
+ $pro->method('reviewPayment')
+ ->willReturn(true);
+
+ $proFactory = $this->getMockBuilder(ProFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $proFactory->method('create')
+ ->willReturn($pro);
+
+ return $proFactory;
+ }
+
+ /**
+ * Gets config mock.
+ *
+ * @return Config|MockObject
+ */
+ protected function getConfig()
+ {
+ $config = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $config->method('getValue')
+ ->with('payment_action')
+ ->willReturn(Config::PAYMENT_ACTION_AUTH);
+
+ return $config;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/approved_case.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/approved_case.php
new file mode 100644
index 0000000000000..eaa3622c04e0e
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/approved_case.php
@@ -0,0 +1,38 @@
+get(CaseInterfaceFactory::class);
+
+$associatedTeam = [
+ 'teamName' => 'Some Team',
+ 'teamId' => 123,
+ 'getAutoDismiss' => true,
+ 'getTeamDismissalDays' => 3
+];
+
+/** @var CaseInterface $case */
+$case = $caseFactory->create();
+$case->setCaseId(123)
+ ->setGuaranteeEligible(false)
+ ->setGuaranteeDisposition(CaseInterface::GUARANTEE_APPROVED)
+ ->setStatus(CaseInterface::STATUS_PROCESSING)
+ ->setScore(553)
+ ->setOrderId($order->getEntityId())
+ ->setAssociatedTeam($associatedTeam)
+ ->setReviewDisposition(CaseInterface::DISPOSITION_GOOD)
+ ->setCreatedAt('2016-12-12T15:17:17+0000')
+ ->setUpdatedAt('2016-12-12T19:23:16+0000');
+
+/** @var CaseRepositoryInterface $caseRepository */
+$caseRepository = $objectManager->get(CaseRepositoryInterface::class);
+$caseRepository->save($case);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/case.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/case.php
new file mode 100644
index 0000000000000..4102233f6fb1f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/case.php
@@ -0,0 +1,42 @@
+create(OrderManagementInterface::class);
+$orderManagement->hold($order->getEntityId());
+
+/** @var CaseInterfaceFactory $caseFactory */
+$caseFactory = $objectManager->get(CaseInterfaceFactory::class);
+
+$associatedTeam = [
+ 'teamName' => 'Some Team',
+ 'teamId' => 123,
+ 'getAutoDismiss' => true,
+ 'getTeamDismissalDays' => 3
+];
+
+/** @var CaseInterface $case */
+$case = $caseFactory->create();
+$case->setCaseId(123)
+ ->setGuaranteeEligible(true)
+ ->setStatus(CaseInterface::STATUS_PROCESSING)
+ ->setScore(553)
+ ->setOrderId($order->getEntityId())
+ ->setAssociatedTeam($associatedTeam)
+ ->setReviewDisposition(CaseInterface::DISPOSITION_GOOD)
+ ->setGuaranteeDisposition(CaseInterface::GUARANTEE_PENDING)
+ ->setCreatedAt('2016-12-12T15:17:17+0000')
+ ->setUpdatedAt('2016-12-12T19:23:16+0000');
+
+/** @var CaseRepositoryInterface $caseRepository */
+$caseRepository = $objectManager->get(CaseRepositoryInterface::class);
+$caseRepository->save($case);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/customer.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/customer.php
new file mode 100644
index 0000000000000..7c5f34cd203fa
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/customer.php
@@ -0,0 +1,38 @@
+get(CustomerRegistry::class);
+$customer = $objectManager->create(Customer::class);
+
+/** @var CustomerInterface $customer */
+$customer->setWebsiteId(1)
+ ->setId(1)
+ ->setEmail('customer@example.com')
+ ->setGroupId(1)
+ ->setStoreId(1)
+ ->setPrefix('Mr.')
+ ->setFirstname('John')
+ ->setMiddlename('A')
+ ->setLastname('Smith')
+ ->setSuffix('Esq.')
+ ->setDefaultBilling(1)
+ ->setDefaultShipping(1)
+ ->setTaxvat('12')
+ ->setGender(0)
+ ->setCreatedAt('2016-12-12T11:00:00+0000')
+ ->setUpdatedAt('2016-12-12T11:05:00+0000');
+
+$customer->isObjectNew(true);
+$customer->save();
+
+$customerRegistry->remove($customer->getId());
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/declined_case.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/declined_case.php
new file mode 100644
index 0000000000000..041cdb0291d83
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/declined_case.php
@@ -0,0 +1,38 @@
+get(CaseInterfaceFactory::class);
+
+$associatedTeam = [
+ 'teamName' => 'Some Team',
+ 'teamId' => 123,
+ 'getAutoDismiss' => true,
+ 'getTeamDismissalDays' => 3
+];
+
+/** @var CaseInterface $case */
+$case = $caseFactory->create();
+$case->setCaseId(123)
+ ->setGuaranteeEligible(false)
+ ->setGuaranteeDisposition(CaseInterface::GUARANTEE_DECLINED)
+ ->setStatus(CaseInterface::STATUS_PROCESSING)
+ ->setScore(553)
+ ->setOrderId($order->getEntityId())
+ ->setAssociatedTeam($associatedTeam)
+ ->setReviewDisposition(CaseInterface::DISPOSITION_FRAUDULENT)
+ ->setCreatedAt('2016-12-12T15:17:17+0000')
+ ->setUpdatedAt('2016-12-12T19:23:16+0000');
+
+/** @var CaseRepositoryInterface $caseRepository */
+$caseRepository = $objectManager->get(CaseRepositoryInterface::class);
+$caseRepository->save($case);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/multiple_cases.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/multiple_cases.php
new file mode 100644
index 0000000000000..4930906954148
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/multiple_cases.php
@@ -0,0 +1,27 @@
+setEntityId(null)
+ ->setIncrementId($order->getIncrementId() + $i);
+
+ $orderRepository->save($newOrder);
+
+ $newCase = clone $case;
+ $newCase->setEntityId(null)
+ ->setCaseId($i)
+ ->setOrderId($newOrder->getEntityId())
+ ->setStatus(CaseInterface::STATUS_OPEN)
+ ->setCreatedAt('2016-12-0' . $i . 'T15:' . $i . ':17+0000')
+ ->setUpdatedAt('2016-12-12T0' . $i . ':23:16+0000')
+ ->setId(null);
+
+ $caseRepository->save($newCase);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
new file mode 100644
index 0000000000000..49a0a2d33e236
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_customer_and_two_simple_products.php
@@ -0,0 +1,119 @@
+create(Address::class, ['data' => $addressData]);
+$billingAddress->setAddressType('billing');
+
+$shippingAddress = clone $billingAddress;
+$shippingAddress->setId(null)
+ ->setAddressType('shipping')
+ ->setStreet(['6161 West Centinela Avenue', 'app. 33'])
+ ->setFirstname('John')
+ ->setLastname('Doe')
+ ->setShippingMethod('flatrate_flatrate');
+
+$payment = $objectManager->create(Payment::class);
+$payment->setMethod('paypal_express')
+ ->setLastTransId('00001')
+ ->setCcLast4('1234')
+ ->setCcExpMonth('01')
+ ->setCcExpYear('21');
+
+/** @var Item $orderItem */
+$orderItem1 = $objectManager->create(Item::class);
+$orderItem1->setProductId($product->getId())
+ ->setSku($product->getSku())
+ ->setName($product->getName())
+ ->setQtyOrdered(1)
+ ->setBasePrice($product->getPrice())
+ ->setPrice($product->getPrice())
+ ->setRowTotal($product->getPrice())
+ ->setProductType($product->getTypeId());
+
+/** @var Item $orderItem */
+$orderItem2 = $objectManager->create(Item::class);
+$orderItem2->setProductId($product->getId())
+ ->setSku('simple2')
+ ->setName('Simple product')
+ ->setPrice(100)
+ ->setQtyOrdered(2)
+ ->setBasePrice($product->getPrice())
+ ->setPrice($product->getPrice())
+ ->setRowTotal($product->getPrice())
+ ->setProductType($product->getTypeId());
+
+$orderAmount = 100;
+$customerEmail = $billingAddress->getEmail();
+
+/** @var Order $order */
+$order = $objectManager->create(Order::class);
+$order->setIncrementId('100000001')
+ ->setState(Order::STATE_PROCESSING)
+ ->setStatus(Order::STATE_PROCESSING)
+ ->setCustomerId($customer->getId())
+ ->setCustomerIsGuest(false)
+ ->setRemoteIp('127.0.0.1')
+ ->setCreatedAt(date('Y-m-d 00:00:55'))
+ ->setOrderCurrencyCode('USD')
+ ->setBaseCurrencyCode('USD')
+ ->setSubtotal($orderAmount)
+ ->setGrandTotal($orderAmount)
+ ->setBaseSubtotal($orderAmount)
+ ->setBaseGrandTotal($orderAmount)
+ ->setCustomerEmail($customerEmail)
+ ->setBillingAddress($billingAddress)
+ ->setShippingAddress($shippingAddress)
+ ->setShippingDescription('Flat Rate - Fixed')
+ ->setShippingAmount(10)
+ ->setStoreId($store->getId())
+ ->addItem($orderItem1)
+ ->addItem($orderItem2)
+ ->setPayment($payment)
+ ->setQuoteId(1);
+
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+$orderRepository->save($order);
+
+$orderAmount2 = 50;
+$payment2 = $objectManager->create(Payment::class);
+$payment2->setMethod('checkmo');
+/** @var Order $order2 */
+$order2 = $objectManager->create(Order::class);
+$order2->setIncrementId('100000005')
+ ->setCustomerId($customer->getId())
+ ->setCustomerIsGuest(false)
+ ->setRemoteIp('127.0.0.1')
+ ->setCreatedAt('2016-12-12T12:00:55+0000')
+ ->setOrderCurrencyCode('USD')
+ ->setBaseCurrencyCode('USD')
+ ->setGrandTotal($orderAmount2)
+ ->setBaseGrandTotal($orderAmount2)
+ ->setCustomerEmail($customerEmail)
+ ->setBillingAddress($billingAddress)
+ ->setShippingAddress($shippingAddress)
+ ->setShippingDescription('Flat Rate - Fixed')
+ ->setShippingAmount(10)
+ ->setStoreId($store->getId())
+ ->addItem($orderItem1)
+ ->setPayment($payment2)
+ ->setQuoteId(2);
+
+$orderRepository->save($order2);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_guest_and_virtual_product.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_guest_and_virtual_product.php
new file mode 100644
index 0000000000000..ba0e92687dc17
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/order_with_guest_and_virtual_product.php
@@ -0,0 +1,68 @@
+create(Address::class, ['data' => $addressData]);
+$billingAddress->setAddressType('billing');
+
+/** @var OrderPaymentInterface $payment */
+$payment = $objectManager->create(Payment::class);
+$payment->setMethod('braintree')
+ ->setLastTransId('00001')
+ ->setAdditionalInformation('avsPostalCodeResponseCode', 'M')
+ ->setAdditionalInformation('avsStreetAddressResponseCode', 'M')
+ ->setAdditionalInformation('cvvResponseCode', 'M');
+
+/** @var Item $orderItem */
+$orderItem1 = $objectManager->create(Item::class);
+$orderItem1->setProductId($product->getId())
+ ->setSku($product->getSku())
+ ->setName($product->getName())
+ ->setQtyOrdered(1)
+ ->setBasePrice($product->getPrice())
+ ->setPrice($product->getPrice())
+ ->setRowTotal($product->getPrice())
+ ->setProductType($product->getTypeId());
+
+$orderAmount = 100;
+$customerEmail = $billingAddress->getEmail();
+
+/** @var Order $order */
+$order = $objectManager->create(Order::class);
+$order->setIncrementId('100000002')
+ ->setState(Order::STATE_PROCESSING)
+ ->setStatus(Order::STATE_PROCESSING)
+ ->setCustomerIsGuest(true)
+ ->setRemoteIp('127.0.0.1')
+ ->setCreatedAt('2016-12-12T12:00:55+0000')
+ ->setOrderCurrencyCode('USD')
+ ->setBaseCurrencyCode('USD')
+ ->setSubtotal($orderAmount)
+ ->setGrandTotal($orderAmount)
+ ->setBaseSubtotal($orderAmount)
+ ->setBaseGrandTotal($orderAmount)
+ ->setCustomerEmail($customerEmail)
+ ->setBillingAddress($billingAddress)
+ ->setStoreId($store->getId())
+ ->addItem($orderItem1)
+ ->setPayment($payment)
+ ->setQuoteId(1);
+
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+$orderRepository->save($order);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/store.php b/dev/tests/integration/testsuite/Magento/Signifyd/_files/store.php
new file mode 100644
index 0000000000000..b814263bdc5ef
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/store.php
@@ -0,0 +1,33 @@
+get(StoreManagerInterface::class)->getStore();
+/** @var MutableScopeConfigInterface $mutableConfig */
+$mutableConfig = $objectManager->get(MutableScopeConfigInterface::class);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_NAME, 'Sample Store', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Store::XML_PATH_UNSECURE_BASE_LINK_URL, 'http://m2.com/', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_ADDRESS1, '6161 West Centinela Avenue', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_ADDRESS2, 'app. 111', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_CITY, 'Culver City', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_REGION_ID, 10, ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_ZIP, '90230', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Shipment::XML_PATH_STORE_COUNTRY_ID, 'US', ScopeInterface::SCOPE_STORE);
+
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_STREET_LINE1, '5th Avenue', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_STREET_LINE2, '75', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_CITY, 'New York', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_REGION_CODE, 30, ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_POSTCODE, '19032', ScopeInterface::SCOPE_STORE);
+$mutableConfig->setValue(Information::XML_PATH_STORE_INFO_COUNTRY_CODE, 'US', ScopeInterface::SCOPE_STORE);
diff --git a/dev/tests/integration/testsuite/Magento/Signifyd/_files/webhook_body.json b/dev/tests/integration/testsuite/Magento/Signifyd/_files/webhook_body.json
new file mode 100644
index 0000000000000..4308c8bf833ef
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Signifyd/_files/webhook_body.json
@@ -0,0 +1 @@
+{"investigationId":123,"analysisUrl":"https://signifyd.com/v2/cases/185088720/analysis","entriesUrl":"https://signifyd.com/v2/cases/185088720/entries","notesUrl":"https://signifyd.com/v2/cases/185088720/notes","orderUrl":"https://signifyd.com/v2/cases/185088720/order","currency":"USD","uuid":"368df42c-d25f-44ef-a1d9-92755f743901","createdAt":"2017-01-06T12:47:03+0000","updatedAt":"2017-01-06T12:47:03+0000","status":"OPEN","caseId":123,"score":384,"headline":"John Doe","orderId":"000000003","adjustedScore":385,"orderDate":"2017-01-06T12:46:58+0000","orderAmount":5.85,"orderOutcome":"SUCCESSFUL","associatedTeam":{"teamName":"Magento","teamId":7940},"testInvestigation":true,"reviewDisposition":null}
\ No newline at end of file
diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Signifyd/frontend/js/Fingerprint.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Signifyd/frontend/js/Fingerprint.test.js
new file mode 100644
index 0000000000000..0be178c5a31f0
--- /dev/null
+++ b/dev/tests/js/jasmine/tests/app/code/Magento/Signifyd/frontend/js/Fingerprint.test.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery'
+], function ($) {
+ 'use strict';
+
+ /*eslint max-nested-callbacks: ["error", 5]*/
+ describe('Signifyd device fingerprint client script', function () {
+
+ it('SIGNIFYD_GLOBAL object initialization check', function (done) {
+ var script = document.createElement('script');
+
+ script.setAttribute('src', 'https://cdn-scripts.signifyd.com/api/script-tag.js');
+ script.setAttribute('id', 'sig-api');
+ script.setAttribute('type', 'text/javascript');
+ script.setAttribute('async', '');
+ script.setAttribute('data-order-session-id', 'mage-jasmin-test');
+
+ $(document.body).append(script);
+
+ setTimeout(function () {
+ var signifyd = window.SIGNIFYD_GLOBAL;
+
+ expect(signifyd).toBeDefined();
+ expect(typeof signifyd).toBe('object');
+ expect(signifyd.scriptTagHasLoaded).toBeDefined();
+ expect(typeof signifyd.scriptTagHasLoaded).toBe('function');
+ expect(signifyd.scriptTagHasLoaded()).toBe(true);
+ done();
+ }, 10000);
+
+ }, 12000);
+ });
+});
diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/exception_hierarchy.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/exception_hierarchy.txt
index 9d7a09f75c2ac..1c96bbce72454 100644
--- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/exception_hierarchy.txt
+++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/blacklist/exception_hierarchy.txt
@@ -10,3 +10,5 @@
\Magento\Framework\DB\DataConverter\DataConversionException
\Magento\Framework\DB\FieldDataConversionException
\Magento\Catalog\Model\Product\Image\NotLoadInfoImageException
+\Magento\Signifyd\Model\SignifydGateway\GatewayException
+\Magento\Signifyd\Model\SignifydGateway\ApiCallException