diff --git a/.htaccess b/.htaccess
index e7a6c2e218cb7..f824f0b7bbc59 100644
--- a/.htaccess
+++ b/.htaccess
@@ -203,76 +203,166 @@
RedirectMatch 403 /\.git
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
# For 404s and 403s that aren't handled by the application, show plain 404 response
diff --git a/.htaccess.sample b/.htaccess.sample
index aa8aaaf9b2818..f3a4474aec949 100644
--- a/.htaccess.sample
+++ b/.htaccess.sample
@@ -111,7 +111,8 @@
############################################
## enable rewrites
- Options +FollowSymLinks
+ # The following line has better security but add some performance overhead - see https://httpd.apache.org/docs/2.4/en/misc/perf-tuning.html
+ Options -FollowSymLinks +SymLinksIfOwnerMatch
RewriteEngine on
############################################
@@ -179,76 +180,166 @@
RedirectMatch 403 /\.git
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
# For 404s and 403s that aren't handled by the application, show plain 404 response
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef841ec0337f2..8270f8908a3c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,693 @@
+2.2.1
+=============
+* GitHub issues:
+ * [#4248](https://github.com/magento/magento2/issues/4248) -- Validations not working on customer registration on DOB field. (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6350](https://github.com/magento/magento2/issues/6350) -- Frontend: Datepicker/calendar control does not use the store locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6858](https://github.com/magento/magento2/issues/6858) -- DatePicker date format does not reflect user's locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6831](https://github.com/magento/magento2/issues/6831) -- Magento 2.1.1 Invalid input date format 'Invalid date' (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#9743](https://github.com/magento/magento2/issues/9743) -- Invalid date when customer validate with French locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6712](https://github.com/magento/magento2/issues/6712) -- Footer Links Widget CSS Issue (fixed in [#11063](https://github.com/magento/magento2/pull/11063))
+ * [#9008](https://github.com/magento/magento2/issues/9008) -- Error Message Is Confusing When Code Base Is Behind Database Module Version (fixed in [#11064](https://github.com/magento/magento2/pull/11064))
+ * [#9981](https://github.com/magento/magento2/issues/9981) -- M2 suggests running setup:upgrade if version number of module is higher than expected (fixed in [#11064](https://github.com/magento/magento2/pull/11064))
+ * [#10824](https://github.com/magento/magento2/issues/10824) -- Cannot add new columns to item grid in admin sales_order_view layout (fixed in [#11076](https://github.com/magento/magento2/pull/11076))
+ * [#10417](https://github.com/magento/magento2/issues/10417) -- Wysywig editor shows broken image icons (fixed in [#11048](https://github.com/magento/magento2/pull/11048))
+ * [#10697](https://github.com/magento/magento2/issues/10697) -- Product Import: Additional data: Invalid URL key (fixed in [#11049](https://github.com/magento/magento2/pull/11049))
+ * [#10474](https://github.com/magento/magento2/issues/10474) -- Error message in product review form not being translated (fixed in [#11069](https://github.com/magento/magento2/pull/11069))
+ * [#9877](https://github.com/magento/magento2/issues/9877) -- getCacheTags for price issue (fixed in [#11154](https://github.com/magento/magento2/pull/11154))
+ * [#10738](https://github.com/magento/magento2/issues/10738) -- Empty attribute label is displayed on product page when other language used. (fixed in [#11168](https://github.com/magento/magento2/pull/11168))
+ * [#9900](https://github.com/magento/magento2/issues/9900) -- Cms module collections missing event prefix (fixed in [#11223](https://github.com/magento/magento2/pull/11223))
+ * [#11044](https://github.com/magento/magento2/issues/11044) -- magento setup:upgrade prompts to run compilation, even in developer mode (fixed in [#11050](https://github.com/magento/magento2/pull/11050))
+ * [#10775](https://github.com/magento/magento2/issues/10775) -- 404 Forbidden sounds not right (fixed in [#11134](https://github.com/magento/magento2/pull/11134))
+ * [#11231](https://github.com/magento/magento2/issues/11231) -- Can't close mobile search bar once typed (fixed in [#11246](https://github.com/magento/magento2/pull/11246))
+ * [#10317](https://github.com/magento/magento2/issues/10317) -- Region is being overridden when changing from a required-state country to one that is not required (fixed in [#11254](https://github.com/magento/magento2/pull/11254))
+ * [#11089](https://github.com/magento/magento2/issues/11089) -- setup:config:set --key append instead of replace (fixed in [#11155](https://github.com/magento/magento2/pull/11155))
+ * [#7582](https://github.com/magento/magento2/issues/7582) -- Payment methods in payments title in wrong language (fixed in [#11165](https://github.com/magento/magento2/pull/11165))
+ * [#5105](https://github.com/magento/magento2/issues/5105) -- Error While send Invoice with Grouped Products (fixed in [#11297](https://github.com/magento/magento2/pull/11297))
+ * [#11163](https://github.com/magento/magento2/issues/11163) -- Magento 2.2.0 Pages showing error: Data key is missing: code-entity (fixed in [#11205](https://github.com/magento/magento2/pull/11205))
+ * [#11329](https://github.com/magento/magento2/issues/11329) -- Unable to proceed massaction "Update attributes" with required multiple select attribute (fixed in [#11349](https://github.com/magento/magento2/pull/11349))
+ * [#8958](https://github.com/magento/magento2/issues/8958) -- Hint mistake in english language (fixed in [#11390](https://github.com/magento/magento2/pull/11390))
+* GitHub pull requests:
+ * [#11067](https://github.com/magento/magento2/pull/11067) -- Fix dob date validation on custom locale (by @joachimVT)
+ * [#11054](https://github.com/magento/magento2/pull/11054) -- Add dev:tests:run parameter to pass arguments to phpunit (by @schmengler)
+ * [#11056](https://github.com/magento/magento2/pull/11056) -- Do not disable maintenance mode after running a backup. (by @stevenvdp)
+ * [#11058](https://github.com/magento/magento2/pull/11058) -- Escape html before replace new line with break (by @Quinten)
+ * [#11063](https://github.com/magento/magento2/pull/11063) -- 6712 Remove additional margin for footer links widget; prevents layou… (by @fragdochkarl)
+ * [#11064](https://github.com/magento/magento2/pull/11064) -- Show different message if DB module version is higher than code modul… (by @schmengler)
+ * [#11076](https://github.com/magento/magento2/pull/11076) -- Backport to 2.2 of #10824: add name for order items grid default renderer block (by @gsomoza)
+ * [#11048](https://github.com/magento/magento2/pull/11048) -- Fix #10417 (by @PieterCappelle)
+ * [#11049](https://github.com/magento/magento2/pull/11049) -- Vague error message for invalid url_key for category (by @avdb)
+ * [#11069](https://github.com/magento/magento2/pull/11069) -- Error message in product review form not being translated (by @Echron)
+ * [#11127](https://github.com/magento/magento2/pull/11127) -- Add missing return statements in setters (by @niccifor)
+ * [#11138](https://github.com/magento/magento2/pull/11138) -- Added template as argument to the store address renderer to allow custom formatting (by @jokeputs)
+ * [#11147](https://github.com/magento/magento2/pull/11147) -- Fix the correct removal of the images and the removal of all images in the catalog (by @raumatbel)
+ * [#11154](https://github.com/magento/magento2/pull/11154) -- Issue #9877: Backport of: getCacheTags for price issue #10930 (by @denysbabenko)
+ * [#11160](https://github.com/magento/magento2/pull/11160) -- Ported Down changes from PR#10919 (by @strell)
+ * [#11200](https://github.com/magento/magento2/pull/11200) -- Delete CallExit function for After plugin logic execution 2.2-develop [BackPort] (by @osrecio)
+ * [#11168](https://github.com/magento/magento2/pull/11168) -- Fixed issue #10738: Don't display attribute label if defined as "none" in layout (by @maksek)
+ * [#11223](https://github.com/magento/magento2/pull/11223) -- Cms page/block eventprefix fix issue 9900 (by @convenient)
+ * [#11229](https://github.com/magento/magento2/pull/11229) -- Disable secret key validation on admin noroute to fix redirect loop (by @convenient)
+ * [#11050](https://github.com/magento/magento2/pull/11050) -- Only prompt for deploy command in production mode (by @schmengler)
+ * [#11134](https://github.com/magento/magento2/pull/11134) -- Fix 404 status header (by @Zifius)
+ * [#11084](https://github.com/magento/magento2/pull/11084) -- Fix in stripped min length validation when value has special characters (by @rubenRP)
+ * [#11246](https://github.com/magento/magento2/pull/11246) -- Can't close mobile search bar (by @crissanclick)
+ * [#11254](https://github.com/magento/magento2/pull/11254) -- Fix for #10317 Disable region list when switching to a region optional country. (by @romainruaud)
+ * [#11155](https://github.com/magento/magento2/pull/11155) -- Refactor ConfigGenerator to replace/set crypt key instead of append (by @renttek)
+ * [#11291](https://github.com/magento/magento2/pull/11291) -- Fix #9243 - Upgrade ZF components. Zend_Service (by @dverkade)
+ * [#11165](https://github.com/magento/magento2/pull/11165) -- #7582: use setStoreId after custom load method to give storeId precedence (by @bka)
+ * [#11297](https://github.com/magento/magento2/pull/11297) -- Fix for issue #5105 - Error While send Invoice with Grouped Products (by @michielgerritsen)
+ * [#11327](https://github.com/magento/magento2/pull/11327) -- Fix #10812: htaccess Options override (by @dverkade)
+ * [#11081](https://github.com/magento/magento2/pull/11081) -- Text updated. (by @RakeshJesadiya)
+ * [#11183](https://github.com/magento/magento2/pull/11183) -- FIX for #11166 Index Handling Fatal Error (by @larsroettig)
+ * [#11205](https://github.com/magento/magento2/pull/11205) -- Fixes #11163 missing data key code-entity in CMS page edit form (by @Tomasz-Silpion)
+ * [#11219](https://github.com/magento/magento2/pull/11219) -- Fix typo in sessionStorage polyfill (by @mszydlo)
+ * [#11249](https://github.com/magento/magento2/pull/11249) -- Add a payload extender to the default shipping-save-processor (Backport to 2.2) (by @navarr)
+ * [#11345](https://github.com/magento/magento2/pull/11345) -- Use US English spelling for "Optimization". (by @davidangel)
+ * [#11349](https://github.com/magento/magento2/pull/11349) -- Bug fix update attributes (by @manuelson)
+ * [#11390](https://github.com/magento/magento2/pull/11390) -- Fix typo in design rule hint message (by @jahvi)
+
+2.2.0
+=============
+* GitHub issues:
+ * [#8287](https://github.com/magento/magento2/issues/8287) -- InputException while updating cart with Minimum order amount enabled (fixed in [#8474](https://github.com/magento/magento2/pull/8474))
+ * [#8315](https://github.com/magento/magento2/issues/8315) -- Error with StdoTest (fixed in [#8487](https://github.com/magento/magento2/pull/8487))
+ * [#5808](https://github.com/magento/magento2/issues/5808) -- [2.1.0] Problem on mobile when catalog gallery allowfullscreen is false (fixed in [#8434](https://github.com/magento/magento2/pull/8434))
+ * [#8392](https://github.com/magento/magento2/issues/8392) -- Initial loading of the related-products sorting fails in Edge-Mode (fixed in [#8467](https://github.com/magento/magento2/pull/8467))
+ * [#8076](https://github.com/magento/magento2/issues/8076) -- Currency Setup in admin throws in_array error when a single value is selected (fixed in [#8077](https://github.com/magento/magento2/pull/8077))
+ * [#8277](https://github.com/magento/magento2/issues/8277) -- CatalogImportExport uploader can't handle HTTPS images (fixed in [#8278](https://github.com/magento/magento2/pull/8278))
+ * [#7353](https://github.com/magento/magento2/issues/7353) -- Crosssells are never shown when using product/list/items.phtml template (fixed in [#8602](https://github.com/magento/magento2/pull/8602))
+ * [#8632](https://github.com/magento/magento2/issues/8632) -- Location of wishlist.js file is incorrect (fixed in [#8633](https://github.com/magento/magento2/pull/8633))
+ * [#6634](https://github.com/magento/magento2/issues/6634) -- Yes/No attribute value is not shown on a product details page (fixed in [#8623](https://github.com/magento/magento2/pull/8623))
+ * [#8566](https://github.com/magento/magento2/issues/8566) -- Setting 'show_out_of_stock' to 'No' has no effect (fixed in [#8736](https://github.com/magento/magento2/pull/8736))
+ * [#6706](https://github.com/magento/magento2/issues/6706) -- Notice undefined index when changes swatch attribute (fixed in [#6707](https://github.com/magento/magento2/pull/6707))
+ * [#6855](https://github.com/magento/magento2/issues/6855) -- Create order via backend doesn't work after using NL translation for order-header (fixed in [#6856](https://github.com/magento/magento2/pull/6856))
+ * [#8515](https://github.com/magento/magento2/issues/8515) -- Downloadable product is available for download even if order state is set canceled. (fixed in [#8917](https://github.com/magento/magento2/pull/8917))
+ * [#8871](https://github.com/magento/magento2/issues/8871) -- Typo in Pull Request Template (fixed in [#8908](https://github.com/magento/magento2/pull/8908))
+ * [#3791](https://github.com/magento/magento2/issues/3791) -- Error review when customer is not logged (fixed in [#9001](https://github.com/magento/magento2/pull/9001))
+ * [#9017](https://github.com/magento/magento2/issues/9017) -- Duplicate call to $this->getLinkField(); (fixed in [#9057](https://github.com/magento/magento2/pull/9057))
+ * [#7498](https://github.com/magento/magento2/issues/7498) -- Grammar Mistake: You don't subscribe to our newsletter. (fixed in [#9080](https://github.com/magento/magento2/pull/9080))
+ * [#9078](https://github.com/magento/magento2/issues/9078) -- Does not translate "Track All Shipments" in My Account, view order (fixed in [#9095](https://github.com/magento/magento2/pull/9095))
+ * [#5731](https://github.com/magento/magento2/issues/5731) -- FPT label not translatable in the totals on the cart page. (fixed in [#9204](https://github.com/magento/magento2/pull/9204))
+ * [#5040](https://github.com/magento/magento2/issues/5040) -- Change 'select' to 'query' in AbstractSearchResult (fixed in [#5043](https://github.com/magento/magento2/pull/5043))
+ * [#8761](https://github.com/magento/magento2/issues/8761) -- Popup-Modal not closing on Safari/Windows (fixed in [#8824](https://github.com/magento/magento2/pull/8824))
+ * [#7549](https://github.com/magento/magento2/issues/7549) -- Google API Tracking code missing single quote after account number (fixed in [#9084](https://github.com/magento/magento2/pull/9084))
+ * [#1146](https://github.com/magento/magento2/issues/1146) -- A few issues when you use /pub as DocumentRoot (fixed in [#9094](https://github.com/magento/magento2/pull/9094))
+ * [#2802](https://github.com/magento/magento2/issues/2802) -- Sitemap generation in wrong folder when vhost is connected to pub folder (fixed in [#9094](https://github.com/magento/magento2/pull/9094))
+ * [#9200](https://github.com/magento/magento2/issues/9200) -- Create new CLI command: Enable DB logging (fixed in [#9264](https://github.com/magento/magento2/pull/9264))
+ * [#9199](https://github.com/magento/magento2/issues/9199) -- Create New CLI command: Generate Varnish VCL file (fixed in [#9286](https://github.com/magento/magento2/pull/9286))
+ * [#6822](https://github.com/magento/magento2/issues/6822) -- CSS Minify Option Breaks inline SVG XML (fixed in [#9027](https://github.com/magento/magento2/pull/9027))
+ * [#8552](https://github.com/magento/magento2/issues/8552) -- CSS Minifying not compatible with CSS3 calc() function (fixed in [#9027](https://github.com/magento/magento2/pull/9027))
+ * [#9236](https://github.com/magento/magento2/issues/9236) -- Upgrade ZF components. Zend_Json (fixed in [#9262](https://github.com/magento/magento2/pull/9262) and [#9261](https://github.com/magento/magento2/pull/9261) and [#9344](https://github.com/magento/magento2/pull/9344) and [#9753](https://github.com/magento/magento2/pull/9753) and [#9754](https://github.com/magento/magento2/pull/9754))
+ * [#7523](https://github.com/magento/magento2/issues/7523) -- Zend_Db_Statement_Exception after refreshing browser with empty category (fixed in [#9400](https://github.com/magento/magento2/pull/9400))
+ * [#9237](https://github.com/magento/magento2/issues/9237) -- Upgrade ZF components. Zend_Log (fixed in [#9285](https://github.com/magento/magento2/pull/9285))
+ * [#9518](https://github.com/magento/magento2/issues/9518) -- Chrome version 58 causes problems with selections in the tinymce editor (fixed in [#9540](https://github.com/magento/magento2/pull/9540))
+ * [#9241](https://github.com/magento/magento2/issues/9241) -- Upgrade ZF components. Zend_Wildfire (fixed in [#9622](https://github.com/magento/magento2/pull/9622))
+ * [#9239](https://github.com/magento/magento2/issues/9239) -- Upgrade ZF components. Zend_Controller (fixed in [#9622](https://github.com/magento/magento2/pull/9622))
+ * [#6455](https://github.com/magento/magento2/issues/6455) -- Cookie Restriction Mode Overlay is cached by Varnish (fixed in [#9711](https://github.com/magento/magento2/pull/9711))
+ * [#5596](https://github.com/magento/magento2/issues/5596) -- Google Universal Analytics does not track when Cookie Restriction is enabled (fixed in [#9713](https://github.com/magento/magento2/pull/9713))
+ * [#6441](https://github.com/magento/magento2/issues/6441) -- Google Analytics Tracking Code cached by Varnish if Cookie Restriction Settings are active (fixed in [#9713](https://github.com/magento/magento2/pull/9713))
+ * [#9242](https://github.com/magento/magento2/issues/9242) -- Upgrade ZF components. Zend_Session (fixed in [#9348](https://github.com/magento/magento2/pull/9348))
+ * [#7279](https://github.com/magento/magento2/issues/7279) -- Bill-to Name and Ship-to Name trancated to 20 characters in backend (fixed in [#9654](https://github.com/magento/magento2/pull/9654))
+ * [#6151](https://github.com/magento/magento2/issues/6151) -- Can't delete last item in cart if Minimum Order is Enable (fixed in [#9714](https://github.com/magento/magento2/pull/9714))
+ * [#4272](https://github.com/magento/magento2/issues/4272) -- v2.0.4 Credit memos with adjustment fees cannot be fully refunded with a second credit memo (fixed in [#9715](https://github.com/magento/magento2/pull/9715))
+ * [#6207](https://github.com/magento/magento2/issues/6207) -- Checkbox IDs for Terms and Conditions should be unique in Checkout (fixed in [#9717](https://github.com/magento/magento2/pull/9717))
+ * [#7844](https://github.com/magento/magento2/issues/7844) -- Customer with unique attribute can't be saved (fixed in [#9712](https://github.com/magento/magento2/pull/9712))
+ * [#6244](https://github.com/magento/magento2/issues/6244) -- Promo code label not used (fixed in [#9721](https://github.com/magento/magento2/pull/9721))
+ * [#9771](https://github.com/magento/magento2/issues/9771) -- XML instruction referenceBlock does not allow template= or does it? (fixed in [#9772](https://github.com/magento/magento2/pull/9772))
+ * [#8221](https://github.com/magento/magento2/issues/8221) -- Javascript "mixins" doesn't works if 'urlArgs' is in requirejs-config.js (fixed in [#9665](https://github.com/magento/magento2/pull/9665))
+ * [#9278](https://github.com/magento/magento2/issues/9278) -- Create new CLI command: Enable Template Hints (fixed in [#9778](https://github.com/magento/magento2/pull/9778))
+ * [#9819](https://github.com/magento/magento2/issues/9819) -- Authentication_lock settings cannot be edited in the Backend (fixed in [#9820](https://github.com/magento/magento2/pull/9820))
+ * [#9216](https://github.com/magento/magento2/issues/9216) -- Coupon codes not showing in invoice print out (fixed in [#9780](https://github.com/magento/magento2/pull/9780))
+ * [#9679](https://github.com/magento/magento2/issues/9679) -- Translation for layered navigation attribute option not working (fixed in [#9873](https://github.com/magento/magento2/pull/9873))
+ * [#3060](https://github.com/magento/magento2/issues/3060) -- setup:static-content:deploy, setup:di:compile and deploy:mode:set will not return a non-zero exit code if any error occurs (fixed in [#7780](https://github.com/magento/magento2/pull/7780))
+ * [#9421](https://github.com/magento/magento2/issues/9421) -- Inconsistent Gift Options checkbox labels (fixed in [#9525](https://github.com/magento/magento2/pull/9525))
+ * [#9805](https://github.com/magento/magento2/issues/9805) -- Static tests in Windows fail due to file path mismatches (fixed in [#9902](https://github.com/magento/magento2/pull/9902))
+ * [#9924](https://github.com/magento/magento2/issues/9924) -- Prefix and suffix are not prefilled in the quote shipping address (fixed in [#9925](https://github.com/magento/magento2/pull/9925))
+ * [#4237](https://github.com/magento/magento2/issues/4237) -- Cron times in the database have a double timezone correction (fixed in [#9943](https://github.com/magento/magento2/pull/9943))
+ * [#9426](https://github.com/magento/magento2/issues/9426) -- Incorrect order date in Orders grid (fixed in [#9941](https://github.com/magento/magento2/pull/9941))
+ * [#3380](https://github.com/magento/magento2/issues/3380) -- Remove scheduled jobs after changing cron settings (fixed in [#9957](https://github.com/magento/magento2/pull/9957))
+ * [#7504](https://github.com/magento/magento2/issues/7504) -- sitemap image URLs do not match with those on product pages (fixed in [#9082](https://github.com/magento/magento2/pull/9082))
+ * [#8732](https://github.com/magento/magento2/issues/8732) -- The country drop-down list display incorrect after upgrade to 2.1.4 in Admin (fixed in [#9429](https://github.com/magento/magento2/pull/9429))
+ * [#9680](https://github.com/magento/magento2/issues/9680) -- Adding product configurations uses sku based name (fixed in [#9681](https://github.com/magento/magento2/pull/9681))
+ * [#10017](https://github.com/magento/magento2/issues/10017) -- Customer Backend: Confirmation Flag is being overwritten by save of the Customer (fixed in [#9681](https://github.com/magento/magento2/pull/9681))
+ * [#2266](https://github.com/magento/magento2/issues/2266) -- Action column menu does not stay over the action column (fixed in [#10082](https://github.com/magento/magento2/pull/10082))
+ * [#5381](https://github.com/magento/magento2/issues/5381) -- Cannot create a `etc/view.xml` file with an `images` tag with an attribute `module` other than `Magento_Catalog` (fixed in [#10052](https://github.com/magento/magento2/pull/10052))
+ * [#6337](https://github.com/magento/magento2/issues/6337) -- Not able to translate page layout head titles (fixed in [#9992](https://github.com/magento/magento2/pull/9992))
+ * [#3872](https://github.com/magento/magento2/issues/3872) -- Slash as category URL suffix gives 404 error on all category pages (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#4660](https://github.com/magento/magento2/issues/4660) -- Multiple URLs causes duplicated content (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#4876](https://github.com/magento/magento2/issues/4876) -- Product URL Suffix "/" results in 404 error (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#8264](https://github.com/magento/magento2/issues/8264) -- Custom URL Rewrite where the request path ends with a forward slash is not matched (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#6396](https://github.com/magento/magento2/issues/6396) -- Cart Price Rules: Category selection UI for Conditions do not come up. (fixed in [#10094](https://github.com/magento/magento2/pull/10094))
+ * [#10124](https://github.com/magento/magento2/issues/10124) -- Wrong order of "width" x "height" when uploading image to admin under Content>Design Config (fixed in [#10126](https://github.com/magento/magento2/pull/10126))
+ * [#6594](https://github.com/magento/magento2/issues/6594) -- Magento 2.1 EE: simplexml_load_string() error in custom widget (fixed in [#10151](https://github.com/magento/magento2/pull/10151))
+ * [#10148](https://github.com/magento/magento2/issues/10148) -- Developer ACL incorrect (fixed in [#10149](https://github.com/magento/magento2/pull/10149))
+ * [#9445](https://github.com/magento/magento2/issues/9445) -- Cart 860 does not contain item 1204 (fixed in [#10059](https://github.com/magento/magento2/pull/10059))
+* GitHub pull requests:
+ * [#4078](https://github.com/magento/magento2/pull/4078) -- Add logo folder to list of allowed resources (by @thaiphan)
+ * [#4355](https://github.com/magento/magento2/pull/4355) -- Enforce password and password-confirm as strings (by @nevvermind)
+ * [#4175](https://github.com/magento/magento2/pull/4175) -- PHP 7 (by @rafaelstz)
+ * [#4669](https://github.com/magento/magento2/pull/4669) -- Php 5.5 support was removed (by @fooman)
+ * [#4766](https://github.com/magento/magento2/pull/4766) -- The time has come for splat operator (by @orlangur)
+ * [#4867](https://github.com/magento/magento2/pull/4867) -- Fix commandExecutor interface mismatch (by @wexo-team)
+ * [#3705](https://github.com/magento/magento2/pull/3705) -- fix issue #3704 regarding integer attribute values being cast to decimal (by @digitalpianism)
+ * [#3750](https://github.com/magento/magento2/pull/3750) -- Add ability to show custom error message on Authorizenet place order (by @ytorbyk)
+ * [#3859](https://github.com/magento/magento2/pull/3859) -- Fixes _toHtml method of Checkout/Block/Cart/Link class (by @ddonnini)
+ * [#4017](https://github.com/magento/magento2/pull/4017) -- Add the $t to translate the message (by @mamzellejuu)
+ * [#4239](https://github.com/magento/magento2/pull/4239) -- Attribute Model specifiable in Propertymapper (by @liolemaire)
+ * [#4583](https://github.com/magento/magento2/pull/4583) -- Fix desktop spelling in lib/web/css #4557 (by @BenSpace48)
+ * [#2735](https://github.com/magento/magento2/pull/2735) -- Add database port to Magento Setup Model Installer (by @tkn98)
+ * [#4275](https://github.com/magento/magento2/pull/4275) -- Database port missing in Magento\TestFramework\Db\Mysql #3529 (by @gordonknoppe)
+ * [#4372](https://github.com/magento/magento2/pull/4372) -- Remove unused property + use ::class (by @nevvermind)
+ * [#2274](https://github.com/magento/magento2/pull/2274) -- TypeError: this.trigger is not a function (by @daim2k5)
+ * [#3050](https://github.com/magento/magento2/pull/3050) -- Replace PrototypeJS (by @srenon)
+ * [#3436](https://github.com/magento/magento2/pull/3436) -- Fix stripped-min-length check (by @avoelkl)
+ * [#3638](https://github.com/magento/magento2/pull/3638) -- Fix #3637 Add missing catalogsearch layout for swatches. (by @romainruaud)
+ * [#3633](https://github.com/magento/magento2/pull/3633) -- #768 - fix Missing acl.xml for the Magento_Checkout module (by @tzyganu)
+ * [#3708](https://github.com/magento/magento2/pull/3708) -- Add 'yyyy' to nomalizedDate method map for adminhtml i18n (by @Amakata)
+ * [#3973](https://github.com/magento/magento2/pull/3973) -- fix bug on newsletter subscription action when user use an existing email already subscribed (by @manfrinm)
+ * [#3932](https://github.com/magento/magento2/pull/3932) -- Update Date.php (by @Maddesto)
+ * [#3907](https://github.com/magento/magento2/pull/3907) -- Update Layout.php (by @Maddesto)
+ * [#4051](https://github.com/magento/magento2/pull/4051) -- Fix "minimum-length" option of the "validate-length" JS validation (by @adragus-inviqa)
+ * [#4269](https://github.com/magento/magento2/pull/4269) -- ConfigurableProduct validator, first check if array item exists (by @BlackIkeEagle)
+ * [#4496](https://github.com/magento/magento2/pull/4496) -- Access element through jQuery (by @sikker)
+ * [#4631](https://github.com/magento/magento2/pull/4631) -- Previous is spelt incorrectly (by @BenSpace48)
+ * [#4882](https://github.com/magento/magento2/pull/4882) -- Fix handling is_region_required key as optional (by @komsitr)
+ * [#5028](https://github.com/magento/magento2/pull/5028) -- Load jquery using requirejs to print page (by @Bartlomiejsz)
+ * [#1957](https://github.com/magento/magento2/pull/1957) -- Add required interface implementation (by @udovicic)
+ * [#2492](https://github.com/magento/magento2/pull/2492) -- No translation from "Orders and Returns" footer links (by @mageho)
+ * [#3749](https://github.com/magento/magento2/pull/3749) -- Stop screen loader on payment error (by @ytorbyk)
+ * [#4901](https://github.com/magento/magento2/pull/4901) -- Fix XML validation value type of post-code to "boolean" (by @adragus-inviqa)
+ * [#5066](https://github.com/magento/magento2/pull/5066) -- Add underscore as dependency to quote.js (by @Bartlomiejsz)
+ * [#5116](https://github.com/magento/magento2/pull/5116) -- Fixes invalid json objects in the order email templates. (by @hostep)
+ * [#5095](https://github.com/magento/magento2/pull/5095) -- Fix integration test expectation to match count of countries in the database (by @Vinai)
+ * [#5200](https://github.com/magento/magento2/pull/5200) -- Adminhtml sales view - Escape remoteIp (by @convenient)
+ * [#4294](https://github.com/magento/magento2/pull/4294) -- Static deploy flags (by @denisristic)
+ * [#3137](https://github.com/magento/magento2/pull/3137) -- Update nginx.conf.sample (by @thaiphan)
+ * [#3746](https://github.com/magento/magento2/pull/3746) -- Add HttpInterface methods and add up-casts for type safety (by @Vinai)
+ * [#4354](https://github.com/magento/magento2/pull/4354) -- Adds test for getDataUsingMethod using digits (by @fooman)
+ * [#4396](https://github.com/magento/magento2/pull/4396) -- Don't hardcode the Magento_Backend::admin index (by @annybs)
+ * [#4491](https://github.com/magento/magento2/pull/4491) -- Correction: variable naming for user roles & role id (by @MagePsycho)
+ * [#4519](https://github.com/magento/magento2/pull/4519) -- Create auth.json.sample (by @rafaelstz)
+ * [#3688](https://github.com/magento/magento2/pull/3688) -- added partial fix for issue #2617. (by @whizkid79)
+ * [#3770](https://github.com/magento/magento2/pull/3770) -- Bugfix: Unable to activate search form on phone (by @vovayatsyuk)
+ * [#4011](https://github.com/magento/magento2/pull/4011) -- Fixed post var name for update attributes (by @Corefix)
+ * [#2791](https://github.com/magento/magento2/pull/2791) -- Update Template.php (by @liam-wiltshire)
+ * [#5496](https://github.com/magento/magento2/pull/5496) -- Fixes #5495 (Mobile navigation submenus need two clicks to open) (by @ajpevers)
+ * [#5725](https://github.com/magento/magento2/pull/5725) -- Fix/translation in validation files (by @dvynograd)
+ * [#5915](https://github.com/magento/magento2/pull/5915) -- Added missing translation to range grid filter (by @maqlec)
+ * [#5884](https://github.com/magento/magento2/pull/5884) -- Use alias already defined in requirejs-config.js (by @kassner)
+ * [#4388](https://github.com/magento/magento2/pull/4388) -- Fixed column description for "website_id" column (by @ikk0)
+ * [#1628](https://github.com/magento/magento2/pull/1628) -- Add cache of configuration files list (by @otakarmare)
+ * [#4791](https://github.com/magento/magento2/pull/4791) -- Replace fabpot/php-cs-fixer with friendsofphp/php-cs-fixer (by @GordonLesti)
+ * [#5983](https://github.com/magento/magento2/pull/5983) -- Fix 'Track your order' i18n. (by @peec)
+ * [#1988](https://github.com/magento/magento2/pull/1988) -- Use inline elements for inline links (by @chicgeek)
+ * [#6283](https://github.com/magento/magento2/pull/6283) -- Update elements.xsd (by @aholovan)
+ * [#1935](https://github.com/magento/magento2/pull/1935) -- Added 'target' attribute to the allowed attributes array for link block (by @fcapua-summa)
+ * [#4733](https://github.com/magento/magento2/pull/4733) -- Escape Js Quote for layout updates (by @bchatard)
+ * [#4565](https://github.com/magento/magento2/pull/4565) -- Update Container.php (by @Maddesto)
+ * [#4845](https://github.com/magento/magento2/pull/4845) -- Set return code for SetModeCommand (by @mc388)
+ * [#2037](https://github.com/magento/magento2/pull/2037) -- Correct the error message when creating wrong object for block (by @hiephm)
+ * [#3779](https://github.com/magento/magento2/pull/3779) -- Add dispatching of view_block_abstract_to_html_after event (by @aleron75)
+ * [#5741](https://github.com/magento/magento2/pull/5741) -- Update AbstractTemplate.php (by @vivek201)
+ * [#5145](https://github.com/magento/magento2/pull/5145) -- \Magento\CatalogInventory\Model\Stock\Status->getStockId() to return correct value (by @fe-lix-)
+ * [#6009](https://github.com/magento/magento2/pull/6009) -- Update README.md (by @fooman)
+ * [#6280](https://github.com/magento/magento2/pull/6280) -- Fixed layout for customer authentication popup (by @rogyar)
+ * [#6549](https://github.com/magento/magento2/pull/6549) -- Remove obsolete comment in catalog_category_view.xml (by @pdanzinger)
+ * [#6804](https://github.com/magento/magento2/pull/6804) -- Fix nav not working in mobile (by @slackerzz)
+ * [#6801](https://github.com/magento/magento2/pull/6801) -- Fixed unclosed span tag in Review module (by @PingusPepan)
+ * [#5045](https://github.com/magento/magento2/pull/5045) -- Fix runner for JsTestDriver based tests on OS X (by @Vinai)
+ * [#4958](https://github.com/magento/magento2/pull/4958) -- block newsletter title (by @slackerzz)
+ * [#5548](https://github.com/magento/magento2/pull/5548) -- Fix error with the WYSIWYG and Greek characters (by @sakisplus)
+ * [#7256](https://github.com/magento/magento2/pull/7256) -- Fix incorrect table name during catalog product indexing (by @nagno)
+ * [#6972](https://github.com/magento/magento2/pull/6972) -- Fix issue #6968 since checkout success title is not displayed (by @AngelVazquezArroyo)
+ * [#4121](https://github.com/magento/magento2/pull/4121) -- Ensure composer.json exists (by @dank00)
+ * [#4134](https://github.com/magento/magento2/pull/4134) -- Added call to action to compile command error (by @sammarcus)
+ * [#6974](https://github.com/magento/magento2/pull/6974) -- Fixed calling non exists order address->getCountry to getCountryId (by @magexo)
+ * [#4088](https://github.com/magento/magento2/pull/4088) -- Fix newsletter queue subscribers adding performance (by @DariuszMaciejewski)
+ * [#6952](https://github.com/magento/magento2/pull/6952) -- Update AccountLock.php (by @klict)
+ * [#5465](https://github.com/magento/magento2/pull/5465) -- Fix Magento\Review\Model\ResourceModel\Rating\Option not instantiable in setup scripts (by @adragus-inviqa)
+ * [#7799](https://github.com/magento/magento2/pull/7799) -- Remove duplicate code from template file. (by @dverkade)
+ * [#7919](https://github.com/magento/magento2/pull/7919) -- Using Dynamic Protocol Concatination (by @brobie)
+ * [#7921](https://github.com/magento/magento2/pull/7921) -- Removed un-used static version rewrite rule in nginx.conf.sample (by @careys7)
+ * [#8019](https://github.com/magento/magento2/pull/8019) -- added fr_CH to allowed locales list (by @annapivniak)
+ * [#8082](https://github.com/magento/magento2/pull/8082) -- issue 8080: Cron configuration loading from DB doesn't work (by @ytorbyk)
+ * [#8062](https://github.com/magento/magento2/pull/8062) -- Fix theme source model used in widget grid (by @Zefiryn)
+ * [#8232](https://github.com/magento/magento2/pull/8232) -- Fix notice during DI compilation (by @qrz-io)
+ * [#8306](https://github.com/magento/magento2/pull/8306) -- Remove warning from setup/src/Magento/Setup/Module/I18n/Dictionary/Writer/Csv.php (by @dmanners)
+ * [#8185](https://github.com/magento/magento2/pull/8185) -- Fix #7461 using the simplest approach for now (by @lazyguru)
+ * [#8183](https://github.com/magento/magento2/pull/8183) -- [BUGFIX] Fixed the credit memo guest email (by @nickgraz)
+ * [#8161](https://github.com/magento/magento2/pull/8161) -- Return focus to search input after closing autocomplete dropdown (by @evktalo)
+ * [#8155](https://github.com/magento/magento2/pull/8155) -- Correct php doc (by @angelomaragna)
+ * [#8341](https://github.com/magento/magento2/pull/8341) -- Use better function for 'Continue Shopping' url (by @dmatthew)
+ * [#8336](https://github.com/magento/magento2/pull/8336) -- fixing time format on admin sales order grid (by @magexo)
+ * [#8327](https://github.com/magento/magento2/pull/8327) -- Change order of parameters passed to LogicException in AbstractTemplate.php (by @bery)
+ * [#8307](https://github.com/magento/magento2/pull/8307) -- Allow digits in communication class type definition (by @cmuench)
+ * [#8354](https://github.com/magento/magento2/pull/8354) -- Display correctly "Add" button label for the block class \Magento\Con… (by @diglin)
+ * [#8246](https://github.com/magento/magento2/pull/8246) -- Fixes #7723 - saving multi select field in UI component form (by @Zefiryn)
+ * [#8353](https://github.com/magento/magento2/pull/8353) -- Replace into the layout adminhtml_order_shipment_new.xml block alias … (by @diglin)
+ * [#8395](https://github.com/magento/magento2/pull/8395) -- Added "editPost" action for customer sections.xml (by @rossluk)
+ * [#8383](https://github.com/magento/magento2/pull/8383) -- Issue/8382 (by @PascalBrouwers)
+ * [#8151](https://github.com/magento/magento2/pull/8151) -- Remove "<2.7" constraint on symfony/console (by @nicolas-grekas)
+ * [#8416](https://github.com/magento/magento2/pull/8416) -- Use configured product attributes in wishlist item collection (by @schmengler)
+ * [#8414](https://github.com/magento/magento2/pull/8414) -- Replace toGMTString with toUTCString (by @schmengler)
+ * [#8419](https://github.com/magento/magento2/pull/8419) -- Use page result instead of rendering layout directly in controllers (by @schmengler)
+ * [#8331](https://github.com/magento/magento2/pull/8331) -- Remove Zend1 Json from Magento Captcha module (by @dmanners)
+ * [#8252](https://github.com/magento/magento2/pull/8252) -- Customer account edit form: additional info block visible (by @Sylvco)
+ * [#8405](https://github.com/magento/magento2/pull/8405) -- Remove the unused Ajax/Serializer.php class (by @dmanners)
+ * [#8402](https://github.com/magento/magento2/pull/8402) -- Remove Zend1 db from captcha module (by @dmanners)
+ * [#8463](https://github.com/magento/magento2/pull/8463) -- Remove outdated comment (by @evktalo)
+ * [#8456](https://github.com/magento/magento2/pull/8456) -- Specifically ask for the Json Serializer object Mage Braintree (by @dmanners)
+ * [#8446](https://github.com/magento/magento2/pull/8446) -- Fix "each()" function call on potentially invalid data (by @giacmir)
+ * [#5928](https://github.com/magento/magento2/pull/5928) -- prevent double shipping method selection (by @danslo)
+ * [#8217](https://github.com/magento/magento2/pull/8217) -- Update DataProvider.php (by @redelschaap)
+ * [#8413](https://github.com/magento/magento2/pull/8413) -- Load translations for area, fixes #8412 (by @fooman)
+ * [#8356](https://github.com/magento/magento2/pull/8356) -- Remove Zend1 captcha from Magento2 Captcha module (by @dmanners)
+ * [#8474](https://github.com/magento/magento2/pull/8474) -- Fix for https://github.com/magento/magento2/issues/8287 (by @ericrisler)
+ * [#8487](https://github.com/magento/magento2/pull/8487) -- Mark the STDO test as skipped (by @dmanners)
+ * [#3155](https://github.com/magento/magento2/pull/3155) -- Remove unused variables in unit test (by @fooman)
+ * [#6049](https://github.com/magento/magento2/pull/6049) -- Added typo correction for table name comment (by @atishgoswami)
+ * [#4370](https://github.com/magento/magento2/pull/4370) -- update Catalog Helper (by @barbarich-p)
+ * [#4106](https://github.com/magento/magento2/pull/4106) -- Adds support for purging varnish cache based on an X-Pool header (by @davidalger)
+ * [#7982](https://github.com/magento/magento2/pull/7982) -- Add bin/magento commands to list store/website data (by @convenient)
+ * [#8434](https://github.com/magento/magento2/pull/8434) -- Problem on mobile when catalog gallery allowfullscreen is false #5808 (by @Crossmotion)
+ * [#8417](https://github.com/magento/magento2/pull/8417) -- add anonymize ip option for google analytics (by @thomas-villagers)
+ * [#8498](https://github.com/magento/magento2/pull/8498) -- Product->save() shouldn't be called directly (by @stansm)
+ * [#8581](https://github.com/magento/magento2/pull/8581) -- Fixed overly bold icons in Firefox Mac (by @TandyCorp)
+ * [#8481](https://github.com/magento/magento2/pull/8481) -- Remove zend json checkout (by @dmanners)
+ * [#8518](https://github.com/magento/magento2/pull/8518) -- Change link text from "Report Bugs" to "Report a Bug" (by @Zifius)
+ * [#8514](https://github.com/magento/magento2/pull/8514) -- Add missing name attributes to catalog_product_view layout xml (by @andrewnoble)
+ * [#8505](https://github.com/magento/magento2/pull/8505) -- Update the Adminhtml image tree JSON (by @dmanners)
+ * [#8467](https://github.com/magento/magento2/pull/8467) -- Fix for magento/magento2#8392 (by @kirashet666)
+ * [#8617](https://github.com/magento/magento2/pull/8617) -- Remove Zend_Json from Customer module (by @dmanners)
+ * [#8609](https://github.com/magento/magento2/pull/8609) -- [PHP 7.1 Compatibility] Void became a reserved word (by @orlangur)
+ * [#8611](https://github.com/magento/magento2/pull/8611) -- Fix #8308: test setup/src/Magento/Setup/Test/Unit/Model/Cron/JobSetCacheTest.php crashes in debug mode (by @orlangur)
+ * [#8610](https://github.com/magento/magento2/pull/8610) -- Fix #8315: test setup/src/Magento/Setup/Test/Unit/Module/I18n/Dictionary/Writer/Csv/StdoTest.php crashes in debug mode (by @orlangur)
+ * [#7339](https://github.com/magento/magento2/pull/7339) -- Fix/xml parser issue (by @dvynograd)
+ * [#7345](https://github.com/magento/magento2/pull/7345) -- Fix typo (by @mpchadwick)
+ * [#7406](https://github.com/magento/magento2/pull/7406) -- MAGETWO-60448 (by @vasilii-b)
+ * [#8000](https://github.com/magento/magento2/pull/8000) -- Set static html fragments as cacheable (by @paveq)
+ * [#8077](https://github.com/magento/magento2/pull/8077) -- Type cast (by @deriknel)
+ * [#8278](https://github.com/magento/magento2/pull/8278) -- bug #8277 fixing bug with https downloading. (by @clementbeudot)
+ * [#8420](https://github.com/magento/magento2/pull/8420) -- Refactor contact module (by @schmengler)
+ * [#8519](https://github.com/magento/magento2/pull/8519) -- Make "is_required" and "is_visible" properties of telephone, company and fax attributes of addresses configurable (by @avstudnitz)
+ * [#8537](https://github.com/magento/magento2/pull/8537) -- Fixed missing echo statement in checkout cart coupon template (by @FrankRuis)
+ * [#8602](https://github.com/magento/magento2/pull/8602) -- Fixed crosssells count always null (by @koenner01)
+ * [#8593](https://github.com/magento/magento2/pull/8593) -- Fix #7371 (by @rossluk)
+ * [#8642](https://github.com/magento/magento2/pull/8642) -- Update Save.php (by @josefbehr)
+ * [#8633](https://github.com/magento/magento2/pull/8633) -- Fix #8632 (by @koenner01)
+ * [#8513](https://github.com/magento/magento2/pull/8513) -- Replace ext name spaces with dashes (as composer does when storing) (by @AydinHassan)
+ * [#8668](https://github.com/magento/magento2/pull/8668) -- Add correct return type in order service (by @cmuench)
+ * [#8677](https://github.com/magento/magento2/pull/8677) -- Fixed Doc Block for the dispatch method of the Rest Controller (by @vrann)
+ * [#3585](https://github.com/magento/magento2/pull/3585) -- Remove duplicate switchIsFilterable (by @GordonLesti)
+ * [#8132](https://github.com/magento/magento2/pull/8132) -- Fix incorrect schema definition for price (by @unfunco)
+ * [#2275](https://github.com/magento/magento2/pull/2275) -- Updated links to point to http://devdocs.magento.com/guides/v2.0 (by @wjarka)
+ * [#8683](https://github.com/magento/magento2/pull/8683) -- Fixed return type of OrderRepository::getList (by @clementbeudot)
+ * [#8682](https://github.com/magento/magento2/pull/8682) -- Remove unused argument (by @mfdj)
+ * [#2185](https://github.com/magento/magento2/pull/2185) -- unused variable (by @barbarich-p)
+ * [#7894](https://github.com/magento/magento2/pull/7894) -- Fix #7893 (by @andreaspenz)
+ * [#8623](https://github.com/magento/magento2/pull/8623) -- Fix check for boolean product attributes (by @TKlement)
+ * [#8678](https://github.com/magento/magento2/pull/8678) -- Fixed Issue #8425 (by @DavidLambauer)
+ * [#8711](https://github.com/magento/magento2/pull/8711) -- Remove Zend_Json from the persistent module remember me status observer (by @dmanners)
+ * [#8706](https://github.com/magento/magento2/pull/8706) -- Remove Zend_Json from the unit test in the CustomerImportExport module (by @dmanners)
+ * [#8723](https://github.com/magento/magento2/pull/8723) -- Add active class to search form wrapper for more theming flexibility (by @andrewkett)
+ * [#8768](https://github.com/magento/magento2/pull/8768) -- Fix issue #8709 (by @renatocason)
+ * [#8762](https://github.com/magento/magento2/pull/8762) -- Fix issue #2558 (by @renatocason)
+ * [#8759](https://github.com/magento/magento2/pull/8759) -- magento/magento2#8618: Apply coupon code button issue on Checkout (by @mcspronko)
+ * [#8776](https://github.com/magento/magento2/pull/8776) -- Range filter doesn't works with 0 values in admin #7103 (by @giacmir)
+ * [#7611](https://github.com/magento/magento2/pull/7611) -- Update catalog_rule_form.xml edit labels (by @fernandofauth)
+ * [#7650](https://github.com/magento/magento2/pull/7650) -- Add host header to varnish cache purge request (by @m0zziter)
+ * [#7914](https://github.com/magento/magento2/pull/7914) -- FPC JS - Fix for CORS issue (by @OZZlE)
+ * [#8013](https://github.com/magento/magento2/pull/8013) -- Update resets.html (by @ryantfowler)
+ * [#8041](https://github.com/magento/magento2/pull/8041) -- Fix USPS Priority Mail to Canada (by @jaywilliams)
+ * [#8014](https://github.com/magento/magento2/pull/8014) -- Update _resets.less (by @ryantfowler)
+ * [#8053](https://github.com/magento/magento2/pull/8053) -- Strict checking types during di compilation (by @michalderlatka)
+ * [#8048](https://github.com/magento/magento2/pull/8048) -- Consistent HTML tags and breaks (by @chickenland)
+ * [#8056](https://github.com/magento/magento2/pull/8056) -- update allowed container tags (by @steros)
+ * [#8158](https://github.com/magento/magento2/pull/8158) -- Fix OAuth request helper to support Authorization header value parsing with non-leading OAuth key (by @careys7)
+ * [#8589](https://github.com/magento/magento2/pull/8589) -- Set correct primary keys for temporary tables in product flat indexer (by @jarnooravainen)
+ * [#8736](https://github.com/magento/magento2/pull/8736) -- Update Stock.php (by @Corefix)
+ * [#8119](https://github.com/magento/magento2/pull/8119) -- Throw exception for invalid (missing) template in dev mode (by @convenient)
+ * [#8690](https://github.com/magento/magento2/pull/8690) -- Fix SALES_ORDER_TAX_ITEM_TAX_ID_ITEM_ID duplicates (by @mimarcel)
+ * [#8743](https://github.com/magento/magento2/pull/8743) -- Validate PHP classnames in di.xml files via schema (by @ktomk)
+ * [#8714](https://github.com/magento/magento2/pull/8714) -- Quote values in IN() predicate to avoid cast issues (by @xi-ao)
+ * [#8835](https://github.com/magento/magento2/pull/8835) -- Replace Zend_Json from the Magento Review module (by @dmanners)
+ * [#8832](https://github.com/magento/magento2/pull/8832) -- Replace Zend_Json from the Magento Quote module (by @dmanners)
+ * [#8839](https://github.com/magento/magento2/pull/8839) -- Rename to DataObjectTest (by @mfdj)
+ * [#2199](https://github.com/magento/magento2/pull/2199) -- Rename admin sidebar Products to Catalog #2060 (by @markoshust)
+ * [#5620](https://github.com/magento/magento2/pull/5620) -- Remove superfluous method call. (by @maksim-grib)
+ * [#6767](https://github.com/magento/magento2/pull/6767) -- Removed setting routerlist twice. (by @dverkade)
+ * [#6257](https://github.com/magento/magento2/pull/6257) -- Fix "none" is not meant to be translated in catalog_product_view layout file (by @azanelli)
+ * [#7645](https://github.com/magento/magento2/pull/7645) -- Storecode in url changed from default to all (by @bartlubbersen)
+ * [#7864](https://github.com/magento/magento2/pull/7864) -- Bugfix: Fix for not respected alternative headers in maintenance mode (by @cmuench)
+ * [#7794](https://github.com/magento/magento2/pull/7794) -- Check return value for getProduct() in getPrice(). (by @pbaylies)
+ * [#8052](https://github.com/magento/magento2/pull/8052) -- Update PurgeCache.php (by @bery)
+ * [#8130](https://github.com/magento/magento2/pull/8130) -- Update Options.php (by @redelschaap)
+ * [#8079](https://github.com/magento/magento2/pull/8079) -- FIX Backend mass delete Sql error (by @asubit)
+ * [#7234](https://github.com/magento/magento2/pull/7234) -- Lossless images optimalization (by @Igloczek)
+ * [#8685](https://github.com/magento/magento2/pull/8685) -- [PSR-2 Compliance] Fix #8612: Hundreds of PHPCS-based static tests violations in mainline (by @orlangur)
+ * [#7568](https://github.com/magento/magento2/pull/7568) -- Minor Update Mysql.php (by @WJdeBaas)
+ * [#7598](https://github.com/magento/magento2/pull/7598) -- Bugfix for _getProductCollection on a product page (by @evgk)
+ * [#8886](https://github.com/magento/magento2/pull/8886) -- Fix typo in app/code/Magento/Email/Test/Unit/Model/TemplateTest.php (by @orlangur)
+ * [#5029](https://github.com/magento/magento2/pull/5029) -- Add default swatch_image placeholder (by @Bartlomiejsz)
+ * [#6838](https://github.com/magento/magento2/pull/6838) -- Removed preference from di.xml (by @dverkade)
+ * [#7578](https://github.com/magento/magento2/pull/7578) -- Don't skip attribute options with a value of 0 from the layered navigation (by @dcabrejas)
+ * [#7615](https://github.com/magento/magento2/pull/7615) -- Fixes duplicate messages shown when adding items to cart (by @comdiler)
+ * [#7590](https://github.com/magento/magento2/pull/7590) -- Reset skippedRows array in clear method (by @ccasciotti)
+ * [#7701](https://github.com/magento/magento2/pull/7701) -- Allows you to have 0 as a option (by @Corefix)
+ * [#7748](https://github.com/magento/magento2/pull/7748) -- Remove _required class from ZIP field (by @dmitryshkolnikov)
+ * [#7743](https://github.com/magento/magento2/pull/7743) -- MAGETWO-61828: Text swatch "zero" not shown (by @oroskodias)
+ * [#8883](https://github.com/magento/magento2/pull/8883) -- Fix a typo (by @evgk)
+ * [#7541](https://github.com/magento/magento2/pull/7541) -- Added "Add / update" comment. Update was missing. (by @gastondisacco)
+ * [#8808](https://github.com/magento/magento2/pull/8808) -- Change link text from "Report a Bug" to "Report an Issue" (by @Zifius)
+ * [#8907](https://github.com/magento/magento2/pull/8907) -- Removed unused dependencys (by @ikrs)
+ * [#8910](https://github.com/magento/magento2/pull/8910) -- Prevent overwriting grunt config (by @Igloczek)
+ * [#7562](https://github.com/magento/magento2/pull/7562) -- Update AbstractCart.php (by @Corefix)
+ * [#8766](https://github.com/magento/magento2/pull/8766) -- fix $childrenWrapClass never used (by @slackerzz)
+ * [#8822](https://github.com/magento/magento2/pull/8822) -- Upgrade PHP CS Fixer to v2 (by @keradus)
+ * [#8901](https://github.com/magento/magento2/pull/8901) -- Update design_config_form.xml (by @WaPoNe)
+ * [#8896](https://github.com/magento/magento2/pull/8896) -- Fix all words without " (by @rafaelstz)
+ * [#8912](https://github.com/magento/magento2/pull/8912) -- magento/magento2#8590: M2.1.4 : ArrayBackend cannot save and Added country regions for Croatia (by @nkajic)
+ * [#8909](https://github.com/magento/magento2/pull/8909) -- magento/magento2#8863: Malta zipcode validation incomplete (by @mmacinko)
+ * [#2829](https://github.com/magento/magento2/pull/2829) -- Update final_price.phtml (by @liam-wiltshire)
+ * [#3869](https://github.com/magento/magento2/pull/3869) -- Optional PHP7.0 Socket Path (by @digimix)
+ * [#4854](https://github.com/magento/magento2/pull/4854) -- Fix admin menu bug (by @NikolasSumrak)
+ * [#6400](https://github.com/magento/magento2/pull/6400) -- Fix for Product Attribute's Conditions (by @NikolasSumrak)
+ * [#6677](https://github.com/magento/magento2/pull/6677) -- Update addCategoriesFilter to return $this (by @maciekpaprocki)
+ * [#6894](https://github.com/magento/magento2/pull/6894) -- Fixed phpseclib\Net\SFTP constants used in write() method (by @federivo)
+ * [#7117](https://github.com/magento/magento2/pull/7117) -- Add de_LU and fr_LU languages for Luxembourg (by @ajpevers)
+ * [#8915](https://github.com/magento/magento2/pull/8915) -- magento/magetno2#8676: I can not translate title attribute in xml fil… (by @DanijelPotocki)
+ * [#5446](https://github.com/magento/magento2/pull/5446) -- Fix Rest Api - GET /V1/configurable-products/{sku}/children not giving ID in response (by @k-andrew)
+ * [#6321](https://github.com/magento/magento2/pull/6321) -- Add missing return in resolveShippingRates (by @GordonLesti)
+ * [#6707](https://github.com/magento/magento2/pull/6707) -- Issue/6706 (by @PascalBrouwers)
+ * [#6794](https://github.com/magento/magento2/pull/6794) -- Move cache to instance of price box widget (by @JamesonNetworks)
+ * [#6856](https://github.com/magento/magento2/pull/6856) -- Issue/6855 (by @PascalBrouwers)
+ * [#6837](https://github.com/magento/magento2/pull/6837) -- Removed argument from di.xml (by @dverkade)
+ * [#6878](https://github.com/magento/magento2/pull/6878) -- Media attribute folder to be ignored (by @rafaelstz)
+ * [#6914](https://github.com/magento/magento2/pull/6914) -- Put the contants in the same docblock together (by @dverkade)
+ * [#6913](https://github.com/magento/magento2/pull/6913) -- Removed commented out line for ignoring coding standards (by @dverkade)
+ * [#7142](https://github.com/magento/magento2/pull/7142) -- [Framework][Translate] Changed translation order to allow language (by @JSchlarb)
+ * [#7352](https://github.com/magento/magento2/pull/7352) -- Product export duplicate rows for product with html special chars in data. Bugfix for #7350 (by @comdiler)
+ * [#8445](https://github.com/magento/magento2/pull/8445) -- Changed name of dispatched object in delete event (by @clementbeudot)
+ * [#8959](https://github.com/magento/magento2/pull/8959) -- Fixes #2461 (by @ajpevers)
+ * [#8950](https://github.com/magento/magento2/pull/8950) -- Remove deadcode (by @jipjop)
+ * [#3469](https://github.com/magento/magento2/pull/3469) -- respect depends declaration in system.xml for form element (by @phoenix-bjoern)
+ * [#5362](https://github.com/magento/magento2/pull/5362) -- Make CMS directive filtering error be more generic (by @erikhansen)
+ * [#6354](https://github.com/magento/magento2/pull/6354) -- Newsletter Email fix (by @inettman)
+ * [#6744](https://github.com/magento/magento2/pull/6744) -- add-product-image-label-bug (by @markpol)
+ * [#6773](https://github.com/magento/magento2/pull/6773) -- Allow to insert Persian characters for attributes! (by @sIiiS)
+ * [#6779](https://github.com/magento/magento2/pull/6779) -- Changed documentation of the cache variable (by @dverkade)
+ * [#6775](https://github.com/magento/magento2/pull/6775) -- Changed constructor to use interface instead of direct classname (by @dverkade)
+ * [#6971](https://github.com/magento/magento2/pull/6971) -- fix #6961 (by @razbakov)
+ * [#7097](https://github.com/magento/magento2/pull/7097) -- You -> Your (by @avitex)
+ * [#7414](https://github.com/magento/magento2/pull/7414) -- Fix typo (by @convenient)
+ * [#8005](https://github.com/magento/magento2/pull/8005) -- Prevent cross origin iframe content reading (by @Igloczek)
+ * [#8753](https://github.com/magento/magento2/pull/8753) -- Added Translation for required Data-Attribute (by @DavidLambauer)
+ * [#8778](https://github.com/magento/magento2/pull/8778) -- magento/magento2: #8765 (by @cavalier79)
+ * [#8914](https://github.com/magento/magento2/pull/8914) -- magento/magetno2#8529:Typo in error message "Table is not exists" (by @bvrbanec)
+ * [#2448](https://github.com/magento/magento2/pull/2448) -- Fix Inconsistency (by @srenon)
+ * [#2093](https://github.com/magento/magento2/pull/2093) -- Remove call to load() in getChildrenCategories method (by @davidalger)
+ * [#4179](https://github.com/magento/magento2/pull/4179) -- fix typo in Magento_Catalog toolbar less source (by @gil--)
+ * [#5078](https://github.com/magento/magento2/pull/5078) -- Rename $websiteId to $scopeId (by @flancer64)
+ * [#5207](https://github.com/magento/magento2/pull/5207) -- Table name fix - rule_customer to salesrule_customer (by @Bartlomiejsz)
+ * [#5858](https://github.com/magento/magento2/pull/5858) -- Save shipping discount in $total (by @flancer64)
+ * [#6811](https://github.com/magento/magento2/pull/6811) -- Unable to save subscription checkbox on Admin customer save (by @rich1990)
+ * [#6839](https://github.com/magento/magento2/pull/6839) -- Changed constructor to use an interface (by @dverkade)
+ * [#6912](https://github.com/magento/magento2/pull/6912) -- Changed module readme text (by @dverkade)
+ * [#7262](https://github.com/magento/magento2/pull/7262) -- Replace boolean cast to be able to disable frame, aspect ratio, trans… (by @joost-florijn-kega)
+ * [#7762](https://github.com/magento/magento2/pull/7762) -- change getId() to getPaymentId() (by @HirokazuNishi)
+ * [#8769](https://github.com/magento/magento2/pull/8769) -- magento/magento2#7860: Invalid comment for the method __order in Mage… (by @mcspronko)
+ * [#8917](https://github.com/magento/magento2/pull/8917) -- imagento/magento2#8515: Downloadable product is available for downloa… (by @nazarpadalka)
+ * [#8908](https://github.com/magento/magento2/pull/8908) -- magento/magento2#8871: Typo in Pull Request Template (by @tomislavsantek)
+ * [#8989](https://github.com/magento/magento2/pull/8989) -- Remove redundant check in if-condition (by @FabianLauer)
+ * [#8953](https://github.com/magento/magento2/pull/8953) -- Log level for caught exception (by @flancer64)
+ * [#8994](https://github.com/magento/magento2/pull/8994) -- Syntax fix (by @rafaelstz)
+ * [#1895](https://github.com/magento/magento2/pull/1895) -- Fix relative template references in individual Magento modules (by @davidalger)
+ * [#4224](https://github.com/magento/magento2/pull/4224) -- Update get.php (by @thaiphan)
+ * [#6567](https://github.com/magento/magento2/pull/6567) -- Always skip hidden files (by @quickshiftin)
+ * [#6989](https://github.com/magento/magento2/pull/6989) -- Allow extending config variables (by @adragus-inviqa)
+ * [#7218](https://github.com/magento/magento2/pull/7218) -- Set store id on block only when empty (by @dank00)
+ * [#7161](https://github.com/magento/magento2/pull/7161) -- Fix a bug resulting in incorrect offsets with dynamic row drag-n-drop functionality (by @navarr)
+ * [#7664](https://github.com/magento/magento2/pull/7664) -- Fix for "Stock Status" field not disappearing in admin when "Manage Stock" = No (by @comdiler)
+ * [#8812](https://github.com/magento/magento2/pull/8812) -- Fix count SQL on products sold collection (by @jameshalsall)
+ * [#8991](https://github.com/magento/magento2/pull/8991) -- Update AbstractModel.php (by @redelschaap)
+ * [#9001](https://github.com/magento/magento2/pull/9001) -- Fix #3791 (by @quienti)
+ * [#8998](https://github.com/magento/magento2/pull/8998) -- Fixed empty submenu group in backend menu (by @vovayatsyuk)
+ * [#8928](https://github.com/magento/magento2/pull/8928) -- Stop $this->validColumnNames array from growing and growing (by @jalogut)
+ * [#4149](https://github.com/magento/magento2/pull/4149) -- Fix wording of downloadable Products (by @bh-ref)
+ * [#4674](https://github.com/magento/magento2/pull/4674) -- Refactor repeating logic (by @nevvermind)
+ * [#4501](https://github.com/magento/magento2/pull/4501) -- Add Crowdin badge - official translations (by @piotrekkaminski)
+ * [#6243](https://github.com/magento/magento2/pull/6243) -- Set getConnection() as public method (by @flancer64)
+ * [#6250](https://github.com/magento/magento2/pull/6250) -- Return the same data on exit (by @flancer64)
+ * [#4844](https://github.com/magento/magento2/pull/4844) -- Fix typo (by @orlangur)
+ * [#4874](https://github.com/magento/magento2/pull/4874) -- Make NL zipcode pattern less strict (by @tdgroot)
+ * [#5400](https://github.com/magento/magento2/pull/5400) -- Privacy Policy translation (fixes Issue #2951) (by @MindConflicts)
+ * [#5671](https://github.com/magento/magento2/pull/5671) -- Fix small typo (by @adragus-inviqa)
+ * [#6132](https://github.com/magento/magento2/pull/6132) -- Remove an extra space while clearing indexed stock items. (by @nntoan)
+ * [#6840](https://github.com/magento/magento2/pull/6840) -- Removed unused variable $routerId (by @dverkade)
+ * [#6834](https://github.com/magento/magento2/pull/6834) -- Removed default values for title, meta description, better labels. (by @paales)
+ * [#7124](https://github.com/magento/magento2/pull/7124) -- Code documentation corrections (by @evktalo)
+ * [#7294](https://github.com/magento/magento2/pull/7294) -- Let less continue compilation if file is empty (by @timo-schmid)
+ * [#8648](https://github.com/magento/magento2/pull/8648) -- Remove the copyright year from file headers (by @jameshalsall)
+ * [#4897](https://github.com/magento/magento2/pull/4897) -- Schedule generation was broken (by @ajpevers)
+ * [#5503](https://github.com/magento/magento2/pull/5503) -- ACL titles are swapped (by @yireo)
+ * [#7344](https://github.com/magento/magento2/pull/7344) -- Fix checking active carrier against store (by @torreytsui)
+ * [#7221](https://github.com/magento/magento2/pull/7221) -- Typo fix (by @gastondisacco)
+ * [#8982](https://github.com/magento/magento2/pull/8982) -- Move blank theme dependencies out of Magento_Theme requirejs-config (by @mikeoloughlin)
+ * [#8930](https://github.com/magento/magento2/pull/8930) -- Improved check when CategoryProcessor attempts to create a new category (by @ccasciotti)
+ * [#8923](https://github.com/magento/magento2/pull/8923) -- Check of null result value in swatch-renderer.js (by @aholovan)
+ * [#9013](https://github.com/magento/magento2/pull/9013) -- Added JS Jasmine tests to Travis CI (by @Igloczek)
+ * [#9039](https://github.com/magento/magento2/pull/9039) -- Translation in adminhtml Import form (by @Nolwennig)
+ * [#9034](https://github.com/magento/magento2/pull/9034) -- Invalidate and refresh customer data sections on HTTP DELETE requests (by @Vinai)
+ * [#6322](https://github.com/magento/magento2/pull/6322) -- Admin product edit block getHeader is not used (by @kassner)
+ * [#7045](https://github.com/magento/magento2/pull/7045) -- In case something wrong with underlying products (by @Will-I4M)
+ * [#8568](https://github.com/magento/magento2/pull/8568) -- Fix quote's outdated shipping address overwriting PayPal Express shipping address (by @torreytsui)
+ * [#9057](https://github.com/magento/magento2/pull/9057) -- remove duplicate method call (#9017) (by @will-b)
+ * [#9080](https://github.com/magento/magento2/pull/9080) -- Fix grammar mistakes with subscriptions - fixes #7498 (by @sambolek)
+ * [#9076](https://github.com/magento/magento2/pull/9076) -- Fix typo (by @PieterCappelle)
+ * [#9061](https://github.com/magento/magento2/pull/9061) -- Empty resolvedScopeCodes when config cache is cleaned (by @andreas-wickberg-vaimo)
+ * [#9044](https://github.com/magento/magento2/pull/9044) -- Remove superfluous character in class (by @tkn98)
+ * [#9095](https://github.com/magento/magento2/pull/9095) -- Added translation to label argument xml. (by @mrkhoa99)
+ * [#9108](https://github.com/magento/magento2/pull/9108) -- Redundant expression */1 in crontab.xml (by @giacmir)
+ * [#9170](https://github.com/magento/magento2/pull/9170) -- Corrected class name in documentation. (by @dfelton)
+ * [#6778](https://github.com/magento/magento2/pull/6778) -- Changed locator class name for ObjectManager (by @dverkade)
+ * [#7556](https://github.com/magento/magento2/pull/7556) -- Fix merging nested in view.xml (by @torreytsui)
+ * [#8903](https://github.com/magento/magento2/pull/8903) -- Include Reply-To name in contact form email header (by @josephmcdermott)
+ * [#9140](https://github.com/magento/magento2/pull/9140) -- remove duplicate calls to initObjectManager in bootstrap class (by @sivajik34)
+ * [#9133](https://github.com/magento/magento2/pull/9133) -- Favicon folder added on gitignore (by @rafaelstz)
+ * [#9204](https://github.com/magento/magento2/pull/9204) -- FPT label not translatable in the totals on the cart page. (by @okorshenko)
+ * [#5043](https://github.com/magento/magento2/pull/5043) -- Change 'select' to 'query' in props (by @flancer64)
+ * [#5367](https://github.com/magento/magento2/pull/5367) -- Change pub/.htaccess MAGE_MODE comment (by @erikhansen)
+ * [#5742](https://github.com/magento/magento2/pull/5742) -- Collection walk method bug fix when specific callback function (by @jalogut)
+ * [#6385](https://github.com/magento/magento2/pull/6385) -- Replace EE License Placeholder text with filename (by @navarr)
+ * [#6443](https://github.com/magento/magento2/pull/6443) -- Refactor Option ResourceModel to allow price supporting types to be intercepted (by @navarr)
+ * [#6772](https://github.com/magento/magento2/pull/6772) -- Changed constructor to use an interface (by @dverkade)
+ * [#6910](https://github.com/magento/magento2/pull/6910) -- Good practice, license in readme file (by @rafaelstz)
+ * [#7506](https://github.com/magento/magento2/pull/7506) -- Add configurations for change email templates (by @kassner)
+ * [#7464](https://github.com/magento/magento2/pull/7464) -- Is Allowed Guest Checkout (by @hungvt)
+ * [#7900](https://github.com/magento/magento2/pull/7900) -- Setting proper resource name (by @ddattee)
+ * [#8462](https://github.com/magento/magento2/pull/8462) -- Fix product option files not copying to order dir. (by @evktalo)
+ * [#8824](https://github.com/magento/magento2/pull/8824) -- Popup-Modal not closing on Safari/Windows (by @Hansschouten)
+ * [#9062](https://github.com/magento/magento2/pull/9062) -- Upgrade JS dependencies (by @Igloczek)
+ * [#9084](https://github.com/magento/magento2/pull/9084) -- Fix Google Analytics typo in printing Account Number, fixes #7549 (by @sambolek)
+ * [#9112](https://github.com/magento/magento2/pull/9112) -- Fix attribute label on product page at different store views (by @tufahu)
+ * [#9103](https://github.com/magento/magento2/pull/9103) -- Cli info di (by @springerin)
+ * [#9165](https://github.com/magento/magento2/pull/9165) -- Improved text of exception message in case of error in module's composer.json (by @vovayatsyuk)
+ * [#9131](https://github.com/magento/magento2/pull/9131) -- Fix to allow Zend_Db_Expr as column default (by @scottsb)
+ * [#9221](https://github.com/magento/magento2/pull/9221) -- Avoid: Undefined index: value in app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php on line 157 in Ajax return (by @mhauri)
+ * [#9215](https://github.com/magento/magento2/pull/9215) -- Remove unused and invalid method (by @adragus-inviqa)
+ * [#9210](https://github.com/magento/magento2/pull/9210) -- Do not di:compile tests/ folder (by @kassner)
+ * [#5325](https://github.com/magento/magento2/pull/5325) -- update url guide to v2.0 (by @sIiiS)
+ * [#6452](https://github.com/magento/magento2/pull/6452) -- Fix Login Popup broken on iPad portrait (by @ihor-sviziev)
+ * [#9234](https://github.com/magento/magento2/pull/9234) -- Fix indentation in pub/.htaccess file (by @erikhansen)
+ * [#6266](https://github.com/magento/magento2/pull/6266) -- Change commit to rollBack on fail (by @flancer64)
+ * [#6792](https://github.com/magento/magento2/pull/6792) -- Make a hardcoded value in Customizable Options interceptable (by @navarr)
+ * [#9094](https://github.com/magento/magento2/pull/9094) -- Issue #2802, #1146: Fixing sitemap generation folder (by @JosephMaxwell)
+ * [#9124](https://github.com/magento/magento2/pull/9124) -- Solve issues with API (by @paales)
+ * [#9247](https://github.com/magento/magento2/pull/9247) -- Fixed layout handle for cms page (by @simpleadm)
+ * [#9257](https://github.com/magento/magento2/pull/9257) -- Fixed coding standard violations in the Framework\Message namespace (by @dverkade)
+ * [#9254](https://github.com/magento/magento2/pull/9254) -- Fixed coding standard violations in the Framework\Encryption namespace (by @dverkade)
+ * [#9253](https://github.com/magento/magento2/pull/9253) -- Fixed coding standard violations in the Framework\Event namespace (by @dverkade)
+ * [#9250](https://github.com/magento/magento2/pull/9250) -- Fixed coding standard violations in the Framework\Archive namespace (by @dverkade)
+ * [#9264](https://github.com/magento/magento2/pull/9264) -- Enable/Disable DB query logging commands (by @federivo)
+ * [#9260](https://github.com/magento/magento2/pull/9260) -- Fixed coding standard violations in Framework\HTTP namespace: (by @dverkade)
+ * [#9258](https://github.com/magento/magento2/pull/9258) -- Fixed coding standard violations in the Framework\Filesystem namespace (by @dverkade)
+ * [#9286](https://github.com/magento/magento2/pull/9286) -- [WIP] Varnish Vcl generator command (by @piotrkwiecinski)
+ * [#9282](https://github.com/magento/magento2/pull/9282) -- Fixed coding standard violations in the Framework\Filter namespace (by @dverkade)
+ * [#9281](https://github.com/magento/magento2/pull/9281) -- Fixed coding standard violations in Framework\Image namespace (by @dverkade)
+ * [#9298](https://github.com/magento/magento2/pull/9298) -- Remove php-5.6 environment from travis.yml (by @4quaternion)
+ * [#4816](https://github.com/magento/magento2/pull/4816) -- Fixed issue with grouped product name column renderer. It (by @wbyrnetx)
+ * [#5147](https://github.com/magento/magento2/pull/5147) -- Change value-assignment to a setValue call (by @wexo-team)
+ * [#5411](https://github.com/magento/magento2/pull/5411) -- #5236 fixes "The configuration parameter "componentType" is a required for "advanced_pricing_button" component" (by @Protazy21)
+ * [#7148](https://github.com/magento/magento2/pull/7148) -- Fix issue 7075 (by @rmsundar1)
+ * [#9027](https://github.com/magento/magento2/pull/9027) -- Make CSS minifying compatible with calc() CSS function (by @sambolek)
+ * [#9262](https://github.com/magento/magento2/pull/9262) -- Remove zend json from theme (by @dmanners)
+ * [#9261](https://github.com/magento/magento2/pull/9261) -- Remove zend json from weee (by @dmanners)
+ * [#9302](https://github.com/magento/magento2/pull/9302) -- Fixed coding standard violations in the Framework\Module namespace (by @dverkade)
+ * [#9299](https://github.com/magento/magento2/pull/9299) -- Improved check for attribute codes in "additional_attributes" field (by @ccasciotti)
+ * [#9293](https://github.com/magento/magento2/pull/9293) -- Deprecate unused navigation-menu.js file in blank theme (by @mikeoloughlin)
+ * [#9308](https://github.com/magento/magento2/pull/9308) -- Remove unnecessary FQCN in ObserverInterface (by @jameshalsall)
+ * [#9304](https://github.com/magento/magento2/pull/9304) -- Fixed coding standard violations in all Factory classes located in app/code (by @dverkade)
+ * [#9303](https://github.com/magento/magento2/pull/9303) -- Fixed coding standard violations in the Framework namespace (by @dverkade)
+ * [#9318](https://github.com/magento/magento2/pull/9318) -- Fixed coding standard violations in the Framework\Backup namespace (by @dverkade)
+ * [#9321](https://github.com/magento/magento2/pull/9321) -- Fixed coding standard violations in the Framework\Autoload, Framework\Session & Framework\Webapi namespaces (by @dverkade)
+ * [#9320](https://github.com/magento/magento2/pull/9320) -- Fixed coding standard violations in the Framework\Cache namespace (by @dverkade)
+ * [#9319](https://github.com/magento/magento2/pull/9319) -- Fixed coding standard violations in the Framework\Api namespace (by @dverkade)
+ * [#9334](https://github.com/magento/magento2/pull/9334) -- Fixed coding standard violations in the Framework\Controller, Framework\CSS, Framework\Phrase and Framework\Pricing namespace (by @dverkade)
+ * [#9330](https://github.com/magento/magento2/pull/9330) -- Fixed coding standard violations in the Framework\Data namespace (by @dverkade)
+ * [#9329](https://github.com/magento/magento2/pull/9329) -- Fixed coding standard violations in the Framework\Stdlib namespace (by @dverkade)
+ * [#9328](https://github.com/magento/magento2/pull/9328) -- Fixed coding standard violations in the Framework\Config namespace (by @dverkade)
+ * [#5372](https://github.com/magento/magento2/pull/5372) -- Fix filesystem permission issues (by @BlackIkeEagle)
+ * [#9129](https://github.com/magento/magento2/pull/9129) -- Product Wizard: Use result type Layout instead of page layout (by @klein0r)
+ * [#9352](https://github.com/magento/magento2/pull/9352) -- Fixed coding standard violations in the Framework\File namespace (by @dverkade)
+ * [#9351](https://github.com/magento/magento2/pull/9351) -- Fixed coding standard violations in the Framework\Locale namespace (by @dverkade)
+ * [#9350](https://github.com/magento/magento2/pull/9350) -- Fixed coding standard violations in the Framework\App namespace (by @dverkade)
+ * [#9355](https://github.com/magento/magento2/pull/9355) -- Fixed coding standard violations in the Framework\Test namespace (by @dverkade)
+ * [#9354](https://github.com/magento/magento2/pull/9354) -- Fixed coding standard violations in the Framework\Translate namespace (by @dverkade)
+ * [#9353](https://github.com/magento/magento2/pull/9353) -- Fixed coding standard violations in the Framework\DB namespace (by @dverkade)
+ * [#8955](https://github.com/magento/magento2/pull/8955) -- Remove context aggregation validation (see Issue #6114) (by @Vinai)
+ * [#9343](https://github.com/magento/magento2/pull/9343) -- Add logging to contact us form (by @JamesonNetworks)
+ * [#9414](https://github.com/magento/magento2/pull/9414) -- Use loadPlayer requirejs mapping (by @ntoombs19)
+ * [#9400](https://github.com/magento/magento2/pull/9400) -- Fix addIdFilter method (by @adrian-martinez-interactiv4)
+ * [#9363](https://github.com/magento/magento2/pull/9363) -- Add ability to inject exception code in LocalizedException (by @adragus-inviqa)
+ * [#9446](https://github.com/magento/magento2/pull/9446) -- Fix data deletion using the multiple delete command (by @Kenboy)
+ * [#9539](https://github.com/magento/magento2/pull/9539) -- fix for "Class Magento\Framework\Console\CLI not found" in case sensitive scenarios (by @EObukhovsky)
+ * [#9514](https://github.com/magento/magento2/pull/9514) -- Fix breadcrumbs extra space (by @VincentMarmiesse)
+ * [#8409](https://github.com/magento/magento2/pull/8409) -- Allow X-Forwarded-For to have multiple values (by @kassner)
+ * [#9093](https://github.com/magento/magento2/pull/9093) -- JS Static tests added to CI (ESLint + JSCS) (by @Igloczek)
+ * [#9091](https://github.com/magento/magento2/pull/9091) -- ESLint errors fix (by @Igloczek)
+ * [#9285](https://github.com/magento/magento2/pull/9285) -- Replace Zend_Log with Psr\Log\LoggerInterface (by @tdgroot)
+ * [#9380](https://github.com/magento/magento2/pull/9380) -- Removed unnecessary code and namespaces from import validators (by @ccasciotti)
+ * [#9540](https://github.com/magento/magento2/pull/9540) -- Removed workaround for old Webkit bug in the TinyMCE editor for selec… (by @hostep)
+ * [#9549](https://github.com/magento/magento2/pull/9549) -- Selects correct stores value option (by @Corefix)
+ * [#9574](https://github.com/magento/magento2/pull/9574) -- no need to create customer once u got the quote object (by @sivajik34)
+ * [#9618](https://github.com/magento/magento2/pull/9618) -- Flip the property assignments for _logger and _fetchStrategy in __wakeup (by @cykirsch)
+ * [#9617](https://github.com/magento/magento2/pull/9617) -- Exclude unnecessarily duplicated Travis CI build jobs (by @Igloczek)
+ * [#9622](https://github.com/magento/magento2/pull/9622) -- Zend_Wildfire deprecated, Firephp outdated, magento/magento2#9239 and magento/magento2#9241 (by @SolsWebdesign)
+ * [#9625](https://github.com/magento/magento2/pull/9625) -- Remove unused plugin (by @elzekool)
+ * [#9637](https://github.com/magento/magento2/pull/9637) -- Change "wan't" to "want" (by @Leland)
+ * [#7020](https://github.com/magento/magento2/pull/7020) -- Fixes #7006, sales_order_status_label does not support version control (by @ajpevers)
+ * [#7456](https://github.com/magento/magento2/pull/7456) -- Remove unused entity_id foreign key (by @mattjbarlow)
+ * [#7755](https://github.com/magento/magento2/pull/7755) -- Remove redundant check and return early (by @AydinHassan)
+ * [#9657](https://github.com/magento/magento2/pull/9657) -- Fixes Typo (by @riconeitzel)
+ * [#4903](https://github.com/magento/magento2/pull/4903) -- Fix undefined offset notice when no order states are set (by @adragus-inviqa)
+ * [#6344](https://github.com/magento/magento2/pull/6344) -- Zend instead of regex in getGetterReturnType (by @flancer64)
+ * [#9686](https://github.com/magento/magento2/pull/9686) -- Update scripts.js (by @redelschaap)
+ * [#9701](https://github.com/magento/magento2/pull/9701) -- Add missing payment info template for PDF generation (by @cmuench)
+ * [#9697](https://github.com/magento/magento2/pull/9697) -- Configure Travis CI to run functional tests (by @okolesnyk)
+ * [#9711](https://github.com/magento/magento2/pull/9711) -- Cookie Restriction Mode Overlay should not be cached by Varnish #6455 (by @bka)
+ * [#9713](https://github.com/magento/magento2/pull/9713) -- stringify cookie value to fix Google Analyitcs Tracking and Cookie Overlay #5596 (by @bka)
+ * [#6503](https://github.com/magento/magento2/pull/6503) -- Remove breadcrumbs for multistore homepage (by @PingusPepan)
+ * [#7330](https://github.com/magento/magento2/pull/7330) -- Fix Framework\Data\Collection::each() method (by @Vinai)
+ * [#8484](https://github.com/magento/magento2/pull/8484) -- Fix swatch-renderer.js product id and isProductViewExist (by @mimarcel)
+ * [#9348](https://github.com/magento/magento2/pull/9348) -- Replace framework's Zend_Session interface usage with SessionHandlerInterface (by @tdgroot)
+ * [#9654](https://github.com/magento/magento2/pull/9654) -- magento/magento2#7279 bill-to name and ship-to name truncated to 20 chars (by @SolsWebdesign)
+ * [#9627](https://github.com/magento/magento2/pull/9627) -- Fix coding standard in Magento AdminNotification module (by @dverkade)
+ * [#9714](https://github.com/magento/magento2/pull/9714) -- Can't delete last item in cart if Minimum Order is Enable #6151 (by @storbahn)
+ * [#9717](https://github.com/magento/magento2/pull/9717) -- use payment method name to make checkbox of agreements more unique #6207 (by @bka)
+ * [#9715](https://github.com/magento/magento2/pull/9715) -- #4272: v2.0.4 Credit memos with adjustment fees cannot be fully refunded with a second credit memo (by @mcspronko)
+ * [#9344](https://github.com/magento/magento2/pull/9344) -- Explace the direct usage of Zend_Json with a call to the Json Help class (by @dmanners)
+ * [#9475](https://github.com/magento/magento2/pull/9475) -- Update select.js (by @redelschaap)
+ * [#9600](https://github.com/magento/magento2/pull/9600) -- Do not hardcode product link types (by @kassner)
+ * [#9712](https://github.com/magento/magento2/pull/9712) -- Customer with unique attribute can't be saved #7844 (by @storbahn)
+ * [#9723](https://github.com/magento/magento2/pull/9723) -- Patch to allow multiple filter_url_params to function (by @southerncomputer)
+ * [#9721](https://github.com/magento/magento2/pull/9721) -- [BUGFIX][6244] Fix Issue with code label display in cart checkout. (by @diglin)
+ * [#9753](https://github.com/magento/magento2/pull/9753) -- Replace Zend_Json in the configurable product block test (by @dmanners)
+ * [#9777](https://github.com/magento/magento2/pull/9777) -- Fix for #5897: getIdentities relies on uninitialized collection (by @kassner)
+ * [#9772](https://github.com/magento/magento2/pull/9772) -- Allow for referenceBlock to include template argument (by @jissereitsma)
+ * [#9797](https://github.com/magento/magento2/pull/9797) -- Adding logo in media folder (by @rafaelstz)
+ * [#9409](https://github.com/magento/magento2/pull/9409) -- Add a name to the Composite\Fieldset\Options block directive (by @navarr)
+ * [#9665](https://github.com/magento/magento2/pull/9665) -- Fix for javascript "mixins" when 'urlArgs' is set in requirejs - issue 8221 (by @thelettuce)
+ * [#9835](https://github.com/magento/magento2/pull/9835) -- Fixes Mage.Cookies poor performance (by @wujashek)
+ * [#9430](https://github.com/magento/magento2/pull/9430) -- Fix wrong store id filter (by @mimarcel)
+ * [#9670](https://github.com/magento/magento2/pull/9670) -- Allow injection of Magento\Catalog\Model\View\Asset\ImageFactory (by @rolftimmermans)
+ * [#9778](https://github.com/magento/magento2/pull/9778) -- new CLI command: Enable Template Hints (by @miguelbalparda)
+ * [#9820](https://github.com/magento/magento2/pull/9820) -- [oauth] Fixes #9819 (by @EliasKotlyar)
+ * [#9859](https://github.com/magento/magento2/pull/9859) -- Removed unused $_customerSession property (by @edenreich)
+ * [#4450](https://github.com/magento/magento2/pull/4450) -- Add ability to use tree-massactions ("sub-menus") on Sales > Orders grid (by @ikk0)
+ * [#9368](https://github.com/magento/magento2/pull/9368) -- Redis sess: fix path for persistent_identifier & compression_threshold (by @LukeHandle)
+ * [#9690](https://github.com/magento/magento2/pull/9690) -- Add froogaloop library as a dependency to load-player module (by @ntoombs19)
+ * [#9813](https://github.com/magento/magento2/pull/9813) -- Use static:: to support late static bindings in Invoice and Creditmemo (by @jokeputs)
+ * [#9780](https://github.com/magento/magento2/pull/9780) -- Coupon codes not showing in invoice print out #9216 (by @naouibelgacem)
+ * [#9872](https://github.com/magento/magento2/pull/9872) -- Fixed issue causing static test failure to report success on Travis (by @davidalger)
+ * [#9890](https://github.com/magento/magento2/pull/9890) -- Improved error logging when trying to save a product (by @woutersamaey)
+ * [#9892](https://github.com/magento/magento2/pull/9892) -- Added .DS_Store to .gitignore for Mac users (by @woutersamaey)
+ * [#9873](https://github.com/magento/magento2/pull/9873) -- Fixes layered navigation options being cached using the wrong store id. (by @hostep)
+ * [#7405](https://github.com/magento/magento2/pull/7405) -- Update Curl.php (by @redelschaap)
+ * [#7780](https://github.com/magento/magento2/pull/7780) -- setup:di:compile returns exit code 0 if errors are found (by @pivulic)
+ * [#9157](https://github.com/magento/magento2/pull/9157) -- Return array of blocks as items instead of array of arrays (by @tkotosz)
+ * [#9810](https://github.com/magento/magento2/pull/9810) -- Fix bug linked product position not updated if product link already exists (by @jalogut)
+ * [#9824](https://github.com/magento/magento2/pull/9824) -- Email to a Friend feature (by @WaPoNe)
+ * [#9823](https://github.com/magento/magento2/pull/9823) -- Return array of pages as items instead of array of arrays (by @tkotosz)
+ * [#9922](https://github.com/magento/magento2/pull/9922) -- Fixes small backwards incompatibility issue created in MAGETWO-69728 (by @hostep)
+ * [#4891](https://github.com/magento/magento2/pull/4891) -- Remove faulty index subscription (by @ajpevers)
+ * [#7758](https://github.com/magento/magento2/pull/7758) -- Throw exception when attribute doesn't exitst (by @AydinHassan)
+ * [#8879](https://github.com/magento/magento2/pull/8879) -- add middle name to checkout address html templates #8878 (by @ajpevers)
+ * [#9251](https://github.com/magento/magento2/pull/9251) -- Fixed coding standard violations in the Framework\Validator namespace (by @dverkade)
+ * [#9525](https://github.com/magento/magento2/pull/9525) -- Fixed the Inconsistent Gift Options checkbox labels #9421 (by @vpiyappan)
+ * [#9905](https://github.com/magento/magento2/pull/9905) -- Fix composer validation (by @barbazul)
+ * [#9932](https://github.com/magento/magento2/pull/9932) -- Fix typo in comment (by @avoelkl)
+ * [#9306](https://github.com/magento/magento2/pull/9306) -- Fix PaymentTokenFactory interface to have the "Interface" at the end of the name. (by @dverkade)
+ * [#9391](https://github.com/magento/magento2/pull/9391) -- Fix depends per group in system.xml (by @osrecio)
+ * [#9902](https://github.com/magento/magento2/pull/9902) -- Fix static integrity classes tests in Windows (by @barbazul)
+ * [#9915](https://github.com/magento/magento2/pull/9915) -- suggestion from #9338: add some command/option to the deploy command to refresh the version (by @ajpevers)
+ * [#9925](https://github.com/magento/magento2/pull/9925) -- Fix #9924, prefill prefix and suffix in checkout shipping address (by @ajpevers)
+ * [#9941](https://github.com/magento/magento2/pull/9941) -- By default, show times in admin grids in the store timezone. (by @ajpevers)
+ * [#9943](https://github.com/magento/magento2/pull/9943) -- Cron uses the wrong timestamp method (by @ajpevers)
+ * [#9964](https://github.com/magento/magento2/pull/9964) -- Add target attribute to Magento_Ui grid (by @thelettuce)
+ * [#9973](https://github.com/magento/magento2/pull/9973) -- Fixed coding standard violations in the Magento\Wishlist namespace (by @dverkade)
+ * [#9974](https://github.com/magento/magento2/pull/9974) -- Fixed coding standard violations in the Magento\Backend namespace (by @dverkade)
+ * [#9975](https://github.com/magento/magento2/pull/9975) -- Fixed coding standard violations in the Magento\Cms namespace (by @dverkade)
+ * [#9978](https://github.com/magento/magento2/pull/9978) -- Fixed coding standard violations in the Magento\Authorization Magento\Backup Magento\Captcha Magento\CurrencySymbol and Magento\Dhl namespace (by @dverkade)
+ * [#8965](https://github.com/magento/magento2/pull/8965) -- Reduce calls to SplFileInfo::realpath() in the Magento\Setup\Module\Di\Code\Reader\ClassesScanner class (by @kschroeder)
+ * [#9996](https://github.com/magento/magento2/pull/9996) -- Ubuntu Trusty 14.04 images update (by @miguelbalparda)
+ * [#8784](https://github.com/magento/magento2/pull/8784) -- magento/magento2: #8616 (by @cavalier79)
+ * [#9939](https://github.com/magento/magento2/pull/9939) -- Retrieve taxes from the correct object (by @fooman)
+ * [#9957](https://github.com/magento/magento2/pull/9957) -- Instantly apply configuration changes in the cron schedule (by @ajpevers)
+ * [#9994](https://github.com/magento/magento2/pull/9994) -- Fix mini-cart not emptied for logged out users checking out with PayPal Express (by @driskell)
+ * [#9082](https://github.com/magento/magento2/pull/9082) -- Get sitemap product images from image cache, if available (by @sambolek)
+ * [#9786](https://github.com/magento/magento2/pull/9786) -- [#7291] Change the default contact form email template to HTML (by @VincentMarmiesse)
+ * [#9361](https://github.com/magento/magento2/pull/9361) -- Fixed coding standard violations in the Framework\Model namespace (by @dverkade)
+ * [#9359](https://github.com/magento/magento2/pull/9359) -- Fixed coding standard violations in the Framework\Interception namespace (by @dverkade)
+ * [#9358](https://github.com/magento/magento2/pull/9358) -- Fixed coding standard violations in the Framework\Code namespace (by @dverkade)
+ * [#9429](https://github.com/magento/magento2/pull/9429) -- Fix not detecting current store using store code in url using $storeResolver->getCurrentStoreId() (by @mimarcel)
+ * [#9362](https://github.com/magento/magento2/pull/9362) -- Fixed coding standard violations in the Framework\ObjectManager namespace (by @dverkade)
+ * [#9970](https://github.com/magento/magento2/pull/9970) -- Added public methods to make Sitemap model plugin friendly (by @7ochem)
+ * [#7729](https://github.com/magento/magento2/pull/7729) -- Allow USPS Shipping Methods Without ExtraServices (by @jaywilliams)
+ * [#9314](https://github.com/magento/magento2/pull/9314) -- Support null value for custom attributes. (by @meng-tian)
+ * [#10033](https://github.com/magento/magento2/pull/10033) -- Fix for file category image uploader (by @Bartlomiejsz)
+ * [#10047](https://github.com/magento/magento2/pull/10047) -- Include attribute code in error message (by @lazyguru)
+ * [#10056](https://github.com/magento/magento2/pull/10056) -- Translate password field placeholder in Checkout (by @mimarcel)
+ * [#7139](https://github.com/magento/magento2/pull/7139) -- Stickyjs improvements (by @vovayatsyuk)
+ * [#9681](https://github.com/magento/magento2/pull/9681) -- Issue 9680: Use parent name for variations (by @PascalBrouwers)
+ * [#10031](https://github.com/magento/magento2/pull/10031) -- Allow option disabling for optgroup binding (by @Bart-art)
+ * [#10060](https://github.com/magento/magento2/pull/10060) -- Adding escapeHtml to Newsletter phtml (by @rafaelstz)
+ * [#10062](https://github.com/magento/magento2/pull/10062) -- Fix formatting for USPS Carrier (by @ihor-sviziev)
+ * [#9672](https://github.com/magento/magento2/pull/9672) -- Revert minimum stability to stable, tasks #4359 (by @ktomk)
+ * [#9986](https://github.com/magento/magento2/pull/9986) -- Improved type hints and declarations for \Magento\Quote\Model\Quote\Address\Total (by @schmengler)
+ * [#10082](https://github.com/magento/magento2/pull/10082) -- M2 2266 (by @tzyganu)
+ * [#10086](https://github.com/magento/magento2/pull/10086) -- Fix condition for autoloader function definitions (by @miromichalicka)
+ * [#9611](https://github.com/magento/magento2/pull/9611) -- Admin Grid Mass action Select / Unselect All issue #9610 (by @minesh0111)
+ * [#9726](https://github.com/magento/magento2/pull/9726) -- Remove wrong '_setup' replace when getting DB connection (2) (by @jalogut)
+ * [#9754](https://github.com/magento/magento2/pull/9754) -- Remove zend json from form elements (by @dmanners)
+ * [#9992](https://github.com/magento/magento2/pull/9992) -- Make page title in layout files translatable (by @ajpevers)
+ * [#10052](https://github.com/magento/magento2/pull/10052) -- M2 5381 (by @tzyganu)
+ * [#1563](https://github.com/magento/magento2/pull/1563) -- Convert long form tags with echo to use short-echo tags (by @davidalger)
+ * [#4147](https://github.com/magento/magento2/pull/4147) -- Add filename paramenter log (by @fcapua-summa)
+ * [#10043](https://github.com/magento/magento2/pull/10043) -- Fix trailing slash used in url rewrites (by @ihor-sviziev)
+ * [#10106](https://github.com/magento/magento2/pull/10106) -- Return URL in getThumbnailUrl instead of nothing (by @samgranger)
+ * [#3889](https://github.com/magento/magento2/pull/3889) -- Add missing dependencies of magento/framework (by @GordonLesti)
+ * [#10114](https://github.com/magento/magento2/pull/10114) -- Add missing dependencies of magento/framework (by @okorshenko)
+ * [#7174](https://github.com/magento/magento2/pull/7174) -- Type hint for \DateTimeInterface instead of \DateTime (by @jameshalsall)
+ * [#9189](https://github.com/magento/magento2/pull/9189) -- Avoid duplicate ltrim function on not complied mode. (by @sivajik34)
+ * [#10094](https://github.com/magento/magento2/pull/10094) -- Fix for isoneof condition in catalogrule (by @duckchip)
+ * [#10105](https://github.com/magento/magento2/pull/10105) -- Add referrerPolicy to Vimeo Video iframe to allow domain-restricted videos (by @davefarthing)
+ * [#10126](https://github.com/magento/magento2/pull/10126) -- Fix width & height mapping during image upload (by @ihor-sviziev)
+ * [#10140](https://github.com/magento/magento2/pull/10140) -- Update attribute vat_id frontend_label to make it translatable (by @JeroenVanLeusden)
+ * [#10149](https://github.com/magento/magento2/pull/10149) -- Fix wrong ACL for Developer Section (by @PascalBrouwers)
+ * [#10151](https://github.com/magento/magento2/pull/10151) -- Fix Widget saving non-XML entities to layout_update (by @tdgroot)
+ * [#9588](https://github.com/magento/magento2/pull/9588) -- Support controller src_type for head links (by @kassner)
+ * [#9904](https://github.com/magento/magento2/pull/9904) -- Fixed pointless exception in logs every time a category with image is saved (by @woutersamaey)
+ * [#10059](https://github.com/magento/magento2/pull/10059) -- Fix fetching quote item by id (by @mladenilic)
+
2.1.0
=============
To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](http://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes")
diff --git a/app/.htaccess b/app/.htaccess
index 93169e4eb44ff..707c26b075e16 100644
--- a/app/.htaccess
+++ b/app/.htaccess
@@ -1,2 +1,8 @@
-Order deny,allow
-Deny from all
+
+ order allow,deny
+ deny from all
+
+= 2.4>
+ Require all denied
+
+
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 6701a9f4dd51e..3d474cea45432 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -49,12 +49,17 @@
unset($_SERVER['ORIG_PATH_INFO']);
}
-if (!empty($_SERVER['MAGE_PROFILER'])
+if (
+ (!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag'))
&& isset($_SERVER['HTTP_ACCEPT'])
&& strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false
) {
+ $profilerFlag = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])
+ ? $_SERVER['MAGE_PROFILER']
+ : trim(file_get_contents(BP . '/var/profiler.flag'));
+
\Magento\Framework\Profiler::applyConfig(
- $_SERVER['MAGE_PROFILER'],
+ $profilerFlag,
BP,
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
);
diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json
index ec54cfd1e5e62..79e6e2d368736 100644
--- a/app/code/Magento/AdvancedPricingImportExport/composer.json
+++ b/app/code/Magento/AdvancedPricingImportExport/composer.json
@@ -13,7 +13,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Analytics/Api/Data/LinkInterface.php b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
new file mode 100644
index 0000000000000..6597dff868b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
@@ -0,0 +1,24 @@
+' . $element->getLabel() . '';
+ $html .= '
';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ return sprintf(
+ ' ',
+ $element->getHtmlId(),
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
new file mode 100644
index 0000000000000..c4118792255cd
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
@@ -0,0 +1,29 @@
+_localeDate->getConfigTimezone();
+ $getLongTimeZoneName = \IntlTimeZone::createTimeZone($timeZoneCode)->getDisplayName();
+ $element->setData(
+ 'comment',
+ sprintf("%s (%s)", $getLongTimeZoneName, $timeZoneCode)
+ );
+ return parent::render($element);
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
new file mode 100644
index 0000000000000..c09213c7f009d
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
@@ -0,0 +1,64 @@
+subscriptionStatusProvider = $labelStatusProvider;
+ }
+
+ /**
+ * Add Subscription status to comment
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $element->setData(
+ 'comment',
+ $this->prepareLabelValue()
+ );
+ return parent::render($element);
+ }
+
+ /**
+ * Prepare label for subscription status
+ *
+ * @return string
+ */
+ private function prepareLabelValue()
+ {
+ return __('Subscription status') . ': ' . __($this->subscriptionStatusProvider->getStatus());
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
new file mode 100644
index 0000000000000..99606e10f99d9
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
@@ -0,0 +1,41 @@
+' . $element->getHint() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * Decorates row HTML for custom element style
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ $rowHtml = sprintf('%s ', $html);
+ $rowHtml .= sprintf(
+ '%s %s ',
+ $element->getHtmlId(),
+ $element->getLabelHtml($element->getHtmlId(), "[WEBSITE]"),
+ $element->getElementHtml()
+ );
+ return $rowHtml;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
new file mode 100644
index 0000000000000..a90a971cf41b4
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
@@ -0,0 +1,64 @@
+config = $config;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::bi_essentials');
+ }
+
+ /**
+ * Provides link to BI Essentials signup
+ *
+ * @return \Magento\Framework\Controller\AbstractResult
+ */
+ public function execute()
+ {
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->config->getValue($this->urlBIEssentialsConfigPath)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
new file mode 100644
index 0000000000000..1b0e5c92420de
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
@@ -0,0 +1,75 @@
+reportUrlProvider = $reportUrlProvider;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller.
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Redirect to resource with reports.
+ *
+ * @return Redirect $resultRedirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setUrl($this->reportUrlProvider->getUrl());
+ } catch (SubscriptionUpdateException $e) {
+ $this->getMessageManager()->addNoticeMessage($e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ $resultRedirect->setPath('adminhtml');
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
new file mode 100644
index 0000000000000..122cf74123cc9
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
@@ -0,0 +1,73 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Retry process of subscription.
+ *
+ * @return Redirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setPath('adminhtml');
+ $this->subscriptionHandler->processEnabled();
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php
new file mode 100644
index 0000000000000..ff0b3e4f67638
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/CollectData.php
@@ -0,0 +1,53 @@
+exportDataHandler = $exportDataHandler;
+ $this->subscriptionStatus = $subscriptionStatus;
+ }
+
+ /**
+ * @return bool
+ */
+ public function execute()
+ {
+ if ($this->subscriptionStatus->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->exportDataHandler->prepareExportData();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/SignUp.php b/app/code/Magento/Analytics/Cron/SignUp.php
new file mode 100644
index 0000000000000..c17b9b8c381c3
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/SignUp.php
@@ -0,0 +1,101 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Execute scheduled subscription operation
+ * In case of failure writes message to notifications inbox
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $attemptsCount = $this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+
+ if (($attemptsCount === null) || ($attemptsCount <= 0)) {
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return false;
+ }
+
+ $attemptsCount -= 1;
+ $this->flagManager->saveFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $signUpResult = $this->connector->execute('signUp');
+ if ($signUpResult === false) {
+ return false;
+ }
+
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return true;
+ }
+
+ /**
+ * Delete cron schedule setting into config.
+ *
+ * Delete cron schedule setting for subscription handler into config and
+ * re-initialize config cache to avoid auto-generate new schedule items.
+ *
+ * @return bool
+ */
+ private function deleteAnalyticsCronExpr()
+ {
+ $this->configWriter->delete(SubscriptionHandler::CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/Update.php b/app/code/Magento/Analytics/Cron/Update.php
new file mode 100644
index 0000000000000..9062a7bac7551
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/Update.php
@@ -0,0 +1,92 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ }
+
+ /**
+ * Execute scheduled update operation
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $attemptsCount = $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+
+ if ($attemptsCount) {
+ $attemptsCount -= 1;
+ $result = $this->connector->execute('update');
+ }
+
+ if ($result || ($attemptsCount <= 0) || (!$this->analyticsToken->isTokenExist())) {
+ $this->flagManager
+ ->deleteFlag(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+ $this->flagManager->deleteFlag(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE);
+ $this->configWriter->delete(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Analytics/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/Analytics/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Analytics/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/Analytics/Model/AnalyticsToken.php b/app/code/Magento/Analytics/Model/AnalyticsToken.php
new file mode 100644
index 0000000000000..ccec4d1bbe958
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/AnalyticsToken.php
@@ -0,0 +1,92 @@
+reinitableConfig = $reinitableConfig;
+ $this->config = $config;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Get Magento BI token value.
+ *
+ * @return string|null
+ */
+ public function getToken()
+ {
+ return $this->config->getValue($this->tokenPath);
+ }
+
+ /**
+ * Stores Magento BI token value.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public function storeToken($value)
+ {
+ $this->configWriter->save($this->tokenPath, $value);
+ $this->reinitableConfig->reinit();
+
+ return true;
+ }
+
+ /**
+ * Check Magento BI token value exist.
+ *
+ * @return bool
+ */
+ public function isTokenExist()
+ {
+ return (bool)$this->getToken();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php
new file mode 100644
index 0000000000000..ba508187b4b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config.php
@@ -0,0 +1,40 @@
+data = $data;
+ }
+
+ /**
+ * Get config value by key.
+ *
+ * @param string|null $key
+ * @param string|null $default
+ * @return array
+ */
+ public function get($key = null, $default = null)
+ {
+ return $this->data->get($key, $default);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
new file mode 100644
index 0000000000000..6e6f008d49f7e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
@@ -0,0 +1,107 @@
+analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Activate process of subscription update handling.
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function processUrlUpdate(string $url)
+ {
+ if ($this->analyticsToken->isTokenExist()) {
+ if (!$this->flagManager->getFlagData(self::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $this->flagManager->saveFlag(self::PREVIOUS_BASE_URL_FLAG_CODE, $url);
+ }
+
+ $this->flagManager
+ ->saveFlag(self::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
new file mode 100644
index 0000000000000..e26ad01fc74bf
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
@@ -0,0 +1,91 @@
+configWriter = $configWriter;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * {@inheritdoc}. Set schedule setting for cron.
+ *
+ * @return Value
+ */
+ public function afterSave()
+ {
+ $result = preg_match('#(?\d{2}),(?\d{2}),(?\d{2})#', $this->getValue(), $time);
+
+ if (!$result) {
+ throw new LocalizedException(__('Time value has an unsupported format'));
+ }
+
+ $cronExprArray = [
+ $time['min'], # Minute
+ $time['hour'], # Hour
+ '*', # Day of the Month
+ '*', # Month of the Year
+ '*', # Day of the Week
+ ];
+
+ $cronExprString = join(' ', $cronExprArray);
+
+ try {
+ $this->configWriter->save(self::CRON_SCHEDULE_PATH, $cronExprString);
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('Cron settings can\'t be saved'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
new file mode 100644
index 0000000000000..ac97f2a843e61
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
@@ -0,0 +1,84 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @return Value
+ * @throws LocalizedException
+ */
+ public function afterSave()
+ {
+ try {
+ if ($this->isValueChanged()) {
+ $enabled = $this->getData('value');
+
+ if ($enabled) {
+ $this->subscriptionHandler->processEnabled();
+ } else {
+ $this->subscriptionHandler->processDisabled();
+ }
+ }
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('There was an error save new configuration value.'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
new file mode 100644
index 0000000000000..4b125949948c6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
@@ -0,0 +1,172 @@
+configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Processing of activation MBI subscription.
+ *
+ * Activate process of subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processEnabled()
+ {
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->setCronSchedule();
+ $this->setAttemptsFlag();
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+
+ /**
+ * Set cron schedule setting into config for activation of subscription process.
+ *
+ * @return bool
+ */
+ private function setCronSchedule()
+ {
+ $this->configWriter->save(self::CRON_STRING_PATH, join(' ', self::CRON_EXPR_ARRAY));
+ return true;
+ }
+
+ /**
+ * Set flag as reserve counter of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function setAttemptsFlag()
+ {
+ return $this->flagManager
+ ->saveFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ }
+
+ /**
+ * Processing of deactivation MBI subscription.
+ *
+ * Disable data collection
+ * and interrupt subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processDisabled()
+ {
+ $this->disableCollectionData();
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->unsetAttemptsFlag();
+ }
+
+ return true;
+ }
+
+ /**
+ * Unset flag of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function unsetAttemptsFlag()
+ {
+ return $this->flagManager
+ ->deleteFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ }
+
+ /**
+ * Unset schedule of collection data cron.
+ *
+ * @return bool
+ */
+ private function disableCollectionData()
+ {
+ $this->configWriter->delete(CollectionTime::CRON_SCHEDULE_PATH);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
new file mode 100644
index 0000000000000..1aabbb91ddf87
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
@@ -0,0 +1,32 @@
+getValue())) {
+ throw new LocalizedException(__('Please select a vertical.'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Mapper.php b/app/code/Magento/Analytics/Model/Config/Mapper.php
new file mode 100644
index 0000000000000..504690b8e4763
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Mapper.php
@@ -0,0 +1,66 @@
+ [
+ * 'name' => 'file_name',
+ * 'providers' => [
+ * 'reportProvider' => [
+ * 'name' => 'report_provider_name',
+ * 'class' => 'Magento\Analytics\ReportXml\ReportProvider',
+ * 'parameters' =>[
+ * 'name' => 'report_name',
+ * ],
+ * ],
+ * 'customProvider' => [
+ * 'name' => 'custom_provider_name',
+ * 'class' => 'Magento\Analytics\Model\CustomProvider',
+ * ],
+ * ],
+ * ]
+ * ];
+ */
+ public function execute($configData)
+ {
+ if (!isset($configData['config'][0]['file'])) {
+ return [];
+ }
+
+ $files = [];
+ foreach ($configData['config'][0]['file'] as $fileData) {
+ /** just one set of providers is allowed by xsd */
+ $providers = reset($fileData['providers']);
+ foreach ($providers as $providerType => $providerDataSet) {
+ /** just one set of provider data is allowed by xsd */
+ $providerData = reset($providerDataSet);
+ /** just one set of parameters is allowed by xsd */
+ $providerData['parameters'] = !empty($providerData['parameters'])
+ ? reset($providerData['parameters'])
+ : [];
+ $providerData['parameters'] = array_map(
+ 'reset',
+ $providerData['parameters']
+ );
+ $providers[$providerType] = $providerData;
+ }
+ $files[$fileData['name']] = $fileData;
+ $files[$fileData['name']]['providers'] = $providers;
+ }
+ return $files;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Reader.php b/app/code/Magento/Analytics/Model/Config/Reader.php
new file mode 100644
index 0000000000000..8980e31627717
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Reader.php
@@ -0,0 +1,52 @@
+mapper = $mapper;
+ $this->readers = $readers;
+ }
+
+ /**
+ * Read configuration scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Source/Vertical.php b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
new file mode 100644
index 0000000000000..c9d9582ea7c7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
@@ -0,0 +1,51 @@
+verticals = $verticals;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toOptionArray()
+ {
+ $result = [
+ ['value' => '', 'label' => __('--Please Select--')]
+ ];
+
+ foreach ($this->verticals as $vertical) {
+ $result[] = ['value' => $vertical, 'label' => __($vertical)];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ConfigInterface.php b/app/code/Magento/Analytics/Model/ConfigInterface.php
new file mode 100644
index 0000000000000..caaa2e100c1c7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ConfigInterface.php
@@ -0,0 +1,22 @@
+ 'command_class_name'.
+ *
+ * The list may be configured in each module via '/etc/di.xml'.
+ *
+ * @var string[]
+ */
+ private $commands;
+
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @param array $commands
+ * @param ObjectManagerInterface $objectManager
+ */
+ public function __construct(
+ array $commands,
+ ObjectManagerInterface $objectManager
+ ) {
+ $this->commands = $commands;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Executes a command in accordance with the given name.
+ *
+ * @param string $commandName
+ * @return bool
+ * @throws NotFoundException if the command is not found.
+ */
+ public function execute($commandName)
+ {
+ if (!array_key_exists($commandName, $this->commands)) {
+ throw new NotFoundException(__('Command was not found.'));
+ }
+
+ /** @var \Magento\Analytics\Model\Connector\CommandInterface $command */
+ $command = $this->objectManager->create($this->commands[$commandName]);
+
+ return $command->execute();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/CommandInterface.php b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
new file mode 100644
index 0000000000000..7a8774fe3dba9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
@@ -0,0 +1,21 @@
+curlFactory = $curlFactory;
+ $this->responseFactory = $responseFactory;
+ $this->converter = $converter;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request($method, $url, array $body = [], array $headers = [], $version = '1.1')
+ {
+ $response = new \Zend_Http_Response(0, []);
+
+ try {
+ $curl = $this->curlFactory->create();
+ $headers = $this->applyContentTypeHeaderFromConverter($headers);
+
+ $curl->write($method, $url, $version, $headers, $this->converter->toBody($body));
+
+ $result = $curl->read();
+
+ if ($curl->getErrno()) {
+ $this->logger->critical(
+ new \Exception(
+ sprintf(
+ 'MBI service CURL connection error #%s: %s',
+ $curl->getErrno(),
+ $curl->getError()
+ )
+ )
+ );
+
+ return $response;
+ }
+
+ $response = $this->responseFactory->create($result);
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param array $headers
+ *
+ * @return array
+ */
+ private function applyContentTypeHeaderFromConverter(array $headers)
+ {
+ $contentTypeHeaderKey = array_search($this->converter->getContentTypeHeader(), $headers);
+ if ($contentTypeHeaderKey === false) {
+ $headers[] = $this->converter->getContentTypeHeader();
+ }
+
+ return $headers;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
new file mode 100644
index 0000000000000..a1e1f057684f6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
@@ -0,0 +1,29 @@
+converter = $converter;
+ $this->responseHandlers = $responseHandlers;
+ }
+
+ /**
+ * @param \Zend_Http_Response $response
+ *
+ * @return bool|string
+ */
+ public function getResult(\Zend_Http_Response $response)
+ {
+ $result = false;
+ $responseBody = $this->converter->fromBody($response->getBody());
+ if (array_key_exists($response->getStatus(), $this->responseHandlers)) {
+ $result = $this->responseHandlers[$response->getStatus()]->handleResponse($responseBody);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
new file mode 100644
index 0000000000000..f1a8ea6460f9d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
@@ -0,0 +1,93 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Notify MBI about that data collection was finished
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->notifyDataChangedUrlPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ }
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/OTPRequest.php b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
new file mode 100644
index 0000000000000..dfa283e10d070
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
@@ -0,0 +1,115 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Performs obtaining of an OTP from the MBI service.
+ *
+ * Returns received OTP or FALSE in case of failure.
+ *
+ * @return string|false
+ */
+ public function call()
+ {
+ $result = false;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->otpUrlConfigPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Obtaining of an OTP from the MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
new file mode 100644
index 0000000000000..d9a672e81f43d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->subscriptionHandler = $subscriptionHandler;
+ $this->subscriptionStatusProvider = $subscriptionStatusProvider;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $responseBody)
+ {
+ if ($this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->analyticsToken->storeToken(null);
+ $this->subscriptionHandler->processEnabled();
+ }
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
new file mode 100644
index 0000000000000..b2261e418abc7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
@@ -0,0 +1,51 @@
+analyticsToken = $analyticsToken;
+ $this->converter = $converter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $body)
+ {
+ if (isset($body['access-token']) && !empty($body['access-token'])) {
+ $this->analyticsToken->storeToken($body['access-token']);
+ return $body['access-token'];
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
new file mode 100644
index 0000000000000..73fc575ae2821
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->integrationManager = $integrationManager;
+ $this->config = $config;
+ $this->httpClient = $httpClient;
+ $this->logger = $logger;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes signUp command
+ *
+ * During this call Magento generates or retrieves access token for the integration user
+ * In case successful generation Magento activates user and sends access token to MA
+ * As the response, Magento receives a token to MA
+ * Magento stores this token in System Configuration
+ *
+ * This method returns true in case of success
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $integrationToken = $this->integrationManager->generateToken();
+ if ($integrationToken) {
+ $this->integrationManager->activateIntegration();
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->signUpUrlPath),
+ [
+ "token" => $integrationToken->getData('token'),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Subscription for MBI service has been failed. An error occurred during token exchange: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
new file mode 100644
index 0000000000000..8f05f1107e87e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
@@ -0,0 +1,114 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->flagManager = $flagManager;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes update request to MBI api in case store url was changed
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::PUT,
+ $this->config->getValue($this->updateUrlPath),
+ [
+ "url" => $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE),
+ "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ "access-token" => $this->analyticsToken->getToken(),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Update of the subscription for MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php
new file mode 100644
index 0000000000000..6905eee372ae2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Cryptographer.php
@@ -0,0 +1,130 @@
+analyticsToken = $analyticsToken;
+ $this->encodedContextFactory = $encodedContextFactory;
+ }
+
+ /**
+ * Encrypt input data.
+ *
+ * @param string $source
+ * @return EncodedContext
+ * @throws LocalizedException
+ */
+ public function encode($source)
+ {
+ if (!is_string($source)) {
+ try {
+ $source = (string)$source;
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('Input data must be string or convertible into string.'));
+ }
+ } elseif (!$source) {
+ throw new LocalizedException(__('Input data must be non-empty string.'));
+ }
+ if (!$this->validateCipherMethod($this->cipherMethod)) {
+ throw new LocalizedException(__('Not valid cipher method.'));
+ }
+ $initializationVector = $this->getInitializationVector();
+
+ $encodedContext = $this->encodedContextFactory->create([
+ 'content' => openssl_encrypt(
+ $source,
+ $this->cipherMethod,
+ $this->getKey(),
+ OPENSSL_RAW_DATA,
+ $initializationVector
+ ),
+ 'initializationVector' => $initializationVector,
+ ]);
+
+ return $encodedContext;
+ }
+
+ /**
+ * Return key for encryption.
+ *
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getKey()
+ {
+ $token = $this->analyticsToken->getToken();
+ if (!$token) {
+ throw new LocalizedException(__('Encryption key can\'t be empty.'));
+ }
+ return hash('sha256', $token);
+ }
+
+ /**
+ * Return established cipher method.
+ *
+ * @return string
+ */
+ private function getCipherMethod()
+ {
+ return $this->cipherMethod;
+ }
+
+ /**
+ * Return each time generated random initialization vector which depends on the cipher method.
+ *
+ * @return string
+ */
+ private function getInitializationVector()
+ {
+ $ivSize = openssl_cipher_iv_length($this->getCipherMethod());
+ return openssl_random_pseudo_bytes($ivSize);
+ }
+
+ /**
+ * Check that cipher method is allowed for encryption.
+ *
+ * @param string $cipherMethod
+ * @return bool
+ */
+ private function validateCipherMethod($cipherMethod)
+ {
+ $methods = openssl_get_cipher_methods();
+ return (false !== array_search($cipherMethod, $methods));
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/EncodedContext.php b/app/code/Magento/Analytics/Model/EncodedContext.php
new file mode 100644
index 0000000000000..5fb2d0c15aef7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/EncodedContext.php
@@ -0,0 +1,52 @@
+content = $content;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
new file mode 100644
index 0000000000000..5d127037afea9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
@@ -0,0 +1,17 @@
+filesystem = $filesystem;
+ $this->archive = $archive;
+ $this->reportWriter = $reportWriter;
+ $this->cryptographer = $cryptographer;
+ $this->fileRecorder = $fileRecorder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepareExportData()
+ {
+ try {
+ $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
+
+ $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath());
+
+ $tmpFilesDirectoryAbsolutePath = $this->validateSource($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $archiveAbsolutePath = $this->prepareFileDirectory($tmpDirectory, $this->getArchiveRelativePath());
+ $this->pack(
+ $tmpFilesDirectoryAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->validateSource($tmpDirectory, $this->getArchiveRelativePath());
+ $this->fileRecorder->recordNewFile(
+ $this->cryptographer->encode($tmpDirectory->readFile($this->getArchiveRelativePath()))
+ );
+ } finally {
+ $tmpDirectory->delete($this->getTmpFilesDirRelativePath());
+ $tmpDirectory->delete($this->getArchiveRelativePath());
+ }
+
+ return true;
+ }
+
+ /**
+ * Return relative path to a directory for temporary files with reports data.
+ *
+ * @return string
+ */
+ private function getTmpFilesDirRelativePath()
+ {
+ return $this->subdirectoryPath . 'tmp/';
+ }
+
+ /**
+ * Return relative path to a directory for an archive.
+ *
+ * @return string
+ */
+ private function getArchiveRelativePath()
+ {
+ return $this->subdirectoryPath . $this->archiveName;
+ }
+
+ /**
+ * Clean up a directory.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Remove a file and a create parent directory a file.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareFileDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+ if (dirname($path) !== '.') {
+ $directory->create(dirname($path));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Packing data into an archive.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return bool
+ */
+ private function pack($source, $destination)
+ {
+ $this->archive->pack(
+ $source,
+ $destination,
+ is_dir($source) ?: false
+ );
+
+ return true;
+ }
+
+ /**
+ * Validate that data source exist.
+ *
+ * Return absolute path in a validated data source.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ * @throws LocalizedException If source is not exist.
+ */
+ private function validateSource(WriteInterface $directory, $path)
+ {
+ if (!$directory->isExist($path)) {
+ throw new LocalizedException(__('Source "%1" is not exist', $directory->getAbsolutePath($path)));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
new file mode 100644
index 0000000000000..65efb33659c89
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
@@ -0,0 +1,19 @@
+exportDataHandler = $exportDataHandler;
+ $this->analyticsConnector = $connector;
+ }
+
+ /**
+ * {@inheritdoc}
+ * Execute notification command.
+ *
+ * @return bool
+ */
+ public function prepareExportData()
+ {
+ $result = $this->exportDataHandler->prepareExportData();
+ $this->analyticsConnector->execute('notifyDataChanged');
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php
new file mode 100644
index 0000000000000..19bdaf21b2a20
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfo.php
@@ -0,0 +1,52 @@
+path = $path;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfoManager.php b/app/code/Magento/Analytics/Model/FileInfoManager.php
new file mode 100644
index 0000000000000..e37700e665420
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfoManager.php
@@ -0,0 +1,123 @@
+flagManager = $flagManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ }
+
+ /**
+ * Save FileInfo object.
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ * @throws LocalizedException
+ */
+ public function save(FileInfo $fileInfo)
+ {
+ $parameters = [];
+ $parameters['initializationVector'] = $fileInfo->getInitializationVector();
+ $parameters['path'] = $fileInfo->getPath();
+
+ $emptyParameters = array_diff($parameters, array_filter($parameters));
+ if ($emptyParameters) {
+ throw new LocalizedException(
+ __('These arguments can\'t be empty "%1"', implode(', ', array_keys($emptyParameters)))
+ );
+ }
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->encodeValue($parameters[$encodedParameter]);
+ }
+
+ $this->flagManager->saveFlag($this->flagCode, $parameters);
+
+ return true;
+ }
+
+ /**
+ * Load FileInfo object.
+ *
+ * @return FileInfo
+ */
+ public function load()
+ {
+ $parameters = $this->flagManager->getFlagData($this->flagCode) ?: [];
+
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($parameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->decodeValue($parameters[$encodedParameter]);
+ }
+
+ $fileInfo = $this->fileInfoFactory->create($parameters);
+
+ return $fileInfo;
+ }
+
+ /**
+ * Encode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function encodeValue($value)
+ {
+ return base64_encode($value);
+ }
+
+ /**
+ * Decode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function decodeValue($value)
+ {
+ return base64_decode($value);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileRecorder.php b/app/code/Magento/Analytics/Model/FileRecorder.php
new file mode 100644
index 0000000000000..70438a98d56f1
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileRecorder.php
@@ -0,0 +1,136 @@
+fileInfoManager = $fileInfoManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Save new encrypted file, register it and remove old registered file.
+ *
+ * @param EncodedContext $encodedContext
+ * @return bool
+ */
+ public function recordNewFile(EncodedContext $encodedContext)
+ {
+ $directory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+
+ $fileRelativePath = $this->getFileRelativePath();
+ $directory->writeFile($fileRelativePath, $encodedContext->getContent());
+
+ $fileInfo = $this->fileInfoManager->load();
+ $this->registerFile($encodedContext, $fileRelativePath);
+ $this->removeOldFile($fileInfo, $directory);
+
+ return true;
+ }
+
+ /**
+ * Return relative path to encoded file.
+ *
+ * @return string
+ */
+ private function getFileRelativePath()
+ {
+ return $this->fileSubdirectoryPath . hash('sha256', time())
+ . '/' . $this->encodedFileName;
+ }
+
+ /**
+ * Register encoded file.
+ *
+ * @param EncodedContext $encodedContext
+ * @param string $fileRelativePath
+ * @return bool
+ */
+ private function registerFile(EncodedContext $encodedContext, $fileRelativePath)
+ {
+ $newFileInfo = $this->fileInfoFactory->create(
+ [
+ 'path' => $fileRelativePath,
+ 'initializationVector' => $encodedContext->getInitializationVector(),
+ ]
+ );
+ $this->fileInfoManager->save($newFileInfo);
+
+ return true;
+ }
+
+ /**
+ * Remove previously registered file.
+ *
+ * @param FileInfo $fileInfo
+ * @param WriteInterface $directory
+ * @return bool
+ */
+ private function removeOldFile(FileInfo $fileInfo, WriteInterface $directory)
+ {
+ if (!$fileInfo->getPath()) {
+ return true;
+ }
+
+ $directory->delete($fileInfo->getPath());
+
+ $directoryName = dirname($fileInfo->getPath());
+ if ($directoryName !== '.') {
+ $directory->delete($directoryName);
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php
new file mode 100644
index 0000000000000..61a40a955e026
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/IntegrationManager.php
@@ -0,0 +1,126 @@
+integrationService = $integrationService;
+ $this->config = $config;
+ $this->oauthService = $oauthService;
+ }
+
+ /**
+ * Activate predefined integration user
+ *
+ * @return bool
+ * @throws NoSuchEntityException
+ */
+ public function activateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ throw new NoSuchEntityException(__('Cannot find predefined integration user!'));
+ }
+ $integrationData = $this->getIntegrationData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = $integration->getId();
+ $this->integrationService->update($integrationData);
+ return true;
+ }
+
+ /**
+ * This method execute Generate Token command and enable integration
+ *
+ * @return bool|\Magento\Integration\Model\Oauth\Token
+ */
+ public function generateToken()
+ {
+ $consumerId = $this->generateIntegration()->getConsumerId();
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ if (!$accessToken && $this->oauthService->createAccessToken($consumerId, true)) {
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ }
+ return $accessToken;
+ }
+
+ /**
+ * Returns consumer Id for MA integration user
+ *
+ * @return \Magento\Integration\Model\Integration
+ */
+ private function generateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ $integration = $this->integrationService->create($this->getIntegrationData());
+ }
+ return $integration;
+ }
+
+ /**
+ * Returns default attributes for MA integration user
+ *
+ * @param int $status
+ * @return array
+ */
+ private function getIntegrationData($status = Integration::STATUS_INACTIVE)
+ {
+ $integrationData = [
+ 'name' => $this->config->getConfigDataValue('analytics/integration_name'),
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ return $integrationData;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Link.php b/app/code/Magento/Analytics/Model/Link.php
new file mode 100644
index 0000000000000..4a40796df4fd0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Link.php
@@ -0,0 +1,54 @@
+url = $url;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/LinkProvider.php b/app/code/Magento/Analytics/Model/LinkProvider.php
new file mode 100644
index 0000000000000..2474653f4916c
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/LinkProvider.php
@@ -0,0 +1,87 @@
+linkFactory = $linkFactory;
+ $this->fileInfoManager = $fileInfoManager;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Returns base url to file according to store configuration
+ *
+ * @param FileInfo $fileInfo
+ * @return string
+ */
+ private function getBaseUrl(FileInfo $fileInfo)
+ {
+ return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $fileInfo->getPath();
+ }
+
+ /**
+ * Verify is requested file ready
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ */
+ private function isFileReady(FileInfo $fileInfo)
+ {
+ return $fileInfo->getPath() && $fileInfo->getInitializationVector();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get()
+ {
+ $fileInfo = $this->fileInfoManager->load();
+ if (!$this->isFileReady($fileInfo)) {
+ throw new NoSuchEntityException(__('File is not ready yet.'));
+ }
+ return $this->linkFactory->create(
+ [
+ 'url' => $this->getBaseUrl($fileInfo),
+ 'initializationVector' => base64_encode($fileInfo->getInitializationVector())
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
new file mode 100644
index 0000000000000..174272614fb19
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
@@ -0,0 +1,61 @@
+subscriptionUpdateHandler = $subscriptionUpdateHandler;
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @param Value $subject
+ * @param Value $result
+ * @return Value
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAfterSave(
+ Value $subject,
+ Value $result
+ ) {
+ if ($this->isPluginApplicable($result)) {
+ $this->subscriptionUpdateHandler->processUrlUpdate($result->getOldValue());
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Value $result
+ * @return bool
+ */
+ private function isPluginApplicable(Value $result)
+ {
+ return $result->isValueChanged()
+ && ($result->getPath() === Store::XML_PATH_SECURE_BASE_URL)
+ && ($result->getScope() === ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php
new file mode 100644
index 0000000000000..3a23430fca077
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ProviderFactory.php
@@ -0,0 +1,39 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * @param string $providerName
+ * @return object
+ */
+ public function create($providerName)
+ {
+ return $this->objectManager->get($providerName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
new file mode 100644
index 0000000000000..e7fdf6f9e8132
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
@@ -0,0 +1,94 @@
+analyticsToken = $analyticsToken;
+ $this->otpRequest = $otpRequest;
+ $this->config = $config;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Provide URL on resource with reports.
+ *
+ * @return string
+ * @throws SubscriptionUpdateException
+ */
+ public function getUrl()
+ {
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ throw new SubscriptionUpdateException(__(
+ 'Your Base URL has been changed and your reports are being updated. '
+ . 'Advanced Reporting will be available once this change has been processed. Please try again later.'
+ ));
+ }
+
+ $url = $this->config->getValue($this->urlReportConfigPath);
+ if ($this->analyticsToken->isTokenExist()) {
+ $otp = $this->otpRequest->call();
+ if ($otp) {
+ $query = http_build_query(['otp' => $otp], '', '&');
+ $url .= '?' . $query;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php
new file mode 100644
index 0000000000000..7128658947908
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriter.php
@@ -0,0 +1,101 @@
+config = $config;
+ $this->reportValidator = $reportValidator;
+ $this->providerFactory = $providerFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write(WriteInterface $directory, $path)
+ {
+ $errorsList = [];
+ foreach ($this->config->get() as $file) {
+ $provider = reset($file['providers']);
+ if (isset($provider['parameters']['name'])) {
+ $error = $this->reportValidator->validate($provider['parameters']['name']);
+ if ($error) {
+ $errorsList[] = $error;
+ continue;
+ }
+ }
+ /** @var $providerObject */
+ $providerObject = $this->providerFactory->create($provider['class']);
+ $fileName = $provider['parameters'] ? $provider['parameters']['name'] : $provider['name'];
+ $fileFullPath = $path . $fileName . '.csv';
+ $fileData = $providerObject->getReport(...array_values($provider['parameters']));
+ $stream = $directory->openFile($fileFullPath, 'w+');
+ $stream->lock();
+ $headers = [];
+ foreach ($fileData as $row) {
+ if (!$headers) {
+ $headers = array_keys($row);
+ $stream->writeCsv($headers);
+ }
+ $stream->writeCsv($row);
+ }
+ $stream->unlock();
+ $stream->close();
+ }
+ if ($errorsList) {
+ $errorStream = $directory->openFile($path . $this->errorsFileName, 'w+');
+ foreach ($errorsList as $error) {
+ $errorStream->lock();
+ $errorStream->writeCsv($error);
+ $errorStream->unlock();
+ }
+ $errorStream->close();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriterInterface.php b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
new file mode 100644
index 0000000000000..a611095a47ae4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
@@ -0,0 +1,28 @@
+moduleManager = $moduleManager;
+ }
+
+ /**
+ * Returns module with module status
+ *
+ * @return array
+ */
+ public function current()
+ {
+ $current = parent::current();
+ if (is_array($current) && isset($current['module_name'])) {
+ $current['status'] =
+ $this->moduleManager->isEnabled($current['module_name']) == 1 ? 'Enabled' : "Disabled";
+ }
+ return $current;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
new file mode 100644
index 0000000000000..0d226a9de7dc2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
@@ -0,0 +1,102 @@
+scopeConfig = $scopeConfig;
+ $this->configPaths = $configPaths;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Generates report using config paths from di.xml
+ * For each website and store
+ * @return \IteratorIterator
+ */
+ public function getReport()
+ {
+ $configReport = $this->generateReportForScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0);
+
+ /** @var WebsiteInterface $website */
+ foreach ($this->storeManager->getWebsites() as $website) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_WEBSITES, $website->getId()),
+ $configReport
+ );
+ }
+
+ /** @var StoreInterface $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_STORES, $store->getId()),
+ $configReport
+ );
+ }
+ return new \IteratorIterator(new \ArrayIterator($configReport));
+ }
+
+ /**
+ * Creates report from config for scope type and scope id.
+ *
+ * @param string $scope
+ * @param int $scopeId
+ * @return array
+ */
+ private function generateReportForScope($scope, $scopeId)
+ {
+ $report = [];
+ foreach ($this->configPaths as $configPath) {
+ $report[] = [
+ "config_path" => $configPath,
+ "scope" => $scope,
+ "scope_id" => $scopeId,
+ "value" => $this->scopeConfig->getValue(
+ $configPath,
+ $scope,
+ $scopeId
+ )
+ ];
+ }
+ return $report;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
new file mode 100644
index 0000000000000..1dd831a672faa
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
@@ -0,0 +1,120 @@
+scopeConfig = $scopeConfig;
+ $this->analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Retrieve subscription status to Magento BI Advanced Reporting.
+ *
+ * Statuses:
+ * Enabled - if subscription is enabled and MA token was received;
+ * Pending - if subscription is enabled and MA token was not received;
+ * Disabled - if subscription is not enabled.
+ * Failed - if subscription is enabled and token was not received after attempts ended.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ $isSubscriptionEnabledInConfig = $this->scopeConfig->getValue('analytics/subscription/enabled');
+ if ($isSubscriptionEnabledInConfig) {
+ return $this->getStatusForEnabledSubscription();
+ }
+
+ return $this->getStatusForDisabledSubscription();
+ }
+
+ /**
+ * Retrieve status for subscription that enabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForEnabledSubscription()
+ {
+ $status = static::ENABLED;
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $status = self::PENDING;
+ }
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $status = static::PENDING;
+ if ($this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) === null) {
+ $status = static::FAILED;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Retrieve status for subscription that disabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForDisabledSubscription()
+ {
+ return static::DISABLED;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
new file mode 100644
index 0000000000000..9aaa2ebb3b56f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
@@ -0,0 +1,80 @@
+subscriptionStatusProvider = $subscriptionStatusProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getIdentity()
+ {
+ return hash('sha256', 'ANALYTICS_NOTIFICATION');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisplayed()
+ {
+ return $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::FAILED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText()
+ {
+ $messageDetails = '';
+
+ $messageDetails .= __('Failed to synchronize data to the Magento Business Intelligence service. ');
+ $messageDetails .= __(
+ 'Retry Synchronization ',
+ $this->urlBuilder->getUrl('analytics/subscription/retry')
+ );
+
+ return $messageDetails;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getSeverity()
+ {
+ return self::SEVERITY_MAJOR;
+ }
+}
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
new file mode 100644
index 0000000000000..7ec64abcd9b86
--- /dev/null
+++ b/app/code/Magento/Analytics/README.md
@@ -0,0 +1,41 @@
+# Magento_Analytics Module
+
+The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality.
+
+The module implements the following functionality:
+
+* enabling subscription to the MBI and automatic re-subscription
+* changing the base URL with the same MBI account remained
+* declaring the configuration schemas for report data collection
+* collecting the Magento instance data as reports for the MBI
+* introducing API that provides the collected data
+* extending Magento configuration with the module parameters:
+ * subscription status (enabled/disabled)
+ * industry (a business area in which the instance website works)
+ * time of data collection (time of the day when the module collects data)
+
+## Structure
+
+Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`.
+[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting.
+The language declares SQL queries using XML declaration.
+
+## Subscription Process
+
+The subscription to the MBI service is enabled during the installation process of the Analytics module. Each administrator will be notified of these new features upon their initial login to the Admin Panel.
+
+## Analytics Settings
+
+Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab.
+
+The following options can be adjusted:
+* Advanced Reporting Service (Enabled/Disabled)
+ * Alters the status of the Advanced Reporting subscription
+* Time of day to send data (Hour/Minute/Second in the store's time zone)
+ * Defines when the data collection process for the Advanced Reporting service occurs
+* Industry
+ * Defines the industry of the store in order to create a personalized Advanced Reporting experience
+
+## Extensibility
+
+We do not recommend to extend the Magento_Analytics module. It introduces an API that is purposed to transfer the collected data. Note that the API cannot be used for other needs.
diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php
new file mode 100644
index 0000000000000..f50dcf941bf50
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config.php
@@ -0,0 +1,43 @@
+data = $data;
+ }
+
+ /**
+ * Returns config value by name
+ *
+ * @param string $queryName
+ * @return array
+ */
+ public function get($queryName)
+ {
+ return $this->data->get($queryName);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
new file mode 100644
index 0000000000000..9e0b20a6ad414
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
@@ -0,0 +1,61 @@
+hasAttributes()) {
+ $attrs = $source->attributes;
+ foreach ($attrs as $attr) {
+ $result[$attr->name] = $attr->value;
+ }
+ }
+ if ($source->hasChildNodes()) {
+ $children = $source->childNodes;
+ if ($children->length == 1) {
+ $child = $children->item(0);
+ if ($child->nodeType == XML_TEXT_NODE) {
+ $result['_value'] = $child->nodeValue;
+ return count($result) == 1 ? $result['_value'] : $result;
+ }
+ }
+ foreach ($children as $child) {
+ if ($child instanceof \DOMCharacterData) {
+ continue;
+ }
+ $result[$child->nodeName][] = $this->convertNode($child);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Converts XML document into corresponding array.
+ *
+ * @param \DOMDocument $source
+ * @return array
+ */
+ public function convert($source)
+ {
+ return $this->convertNode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Mapper.php b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
new file mode 100644
index 0000000000000..4dda8f3c733a6
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
@@ -0,0 +1,37 @@
+readers = $readers;
+ $this->mapper = $mapper;
+ }
+
+ /**
+ * Reads configuration according to the given scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
new file mode 100644
index 0000000000000..ec03ddf429c06
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
@@ -0,0 +1,23 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates one-time connection for export
+ *
+ * @param string $connectionName
+ * @return AdapterInterface
+ */
+ public function getConnection($connectionName)
+ {
+ $connection = $this->resourceConnection->getConnection($connectionName);
+ $connectionClassName = get_class($connection);
+ $configData = $connection->getConfig();
+ $configData['use_buffered_query'] = false;
+ unset($configData['persistent']);
+ return $this->objectManager->create(
+ $connectionClassName,
+ [
+ 'config' => $configData
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
new file mode 100644
index 0000000000000..083b4843c185a
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
@@ -0,0 +1,27 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ }
+
+ /**
+ * Assembles WHERE conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['filter'])) {
+ return $selectBuilder;
+ }
+ $filters = $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $queryConfig['source']['filter'],
+ $this->nameResolver->getAlias($queryConfig['source'])
+ );
+ $selectBuilder->setFilters(array_merge_recursive($selectBuilder->getFilters(), [$filters]));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
new file mode 100644
index 0000000000000..811119ace221b
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
@@ -0,0 +1,69 @@
+nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles FROM condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ $selectBuilder->setFrom(
+ [
+ $this->nameResolver->getAlias($queryConfig['source']) =>
+ $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($queryConfig['source'])),
+ ]
+ );
+ $columns = $this->columnsResolver->getColumns($selectBuilder, $queryConfig['source']);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
new file mode 100644
index 0000000000000..f3c6540a25171
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
@@ -0,0 +1,113 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles JOIN conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['link-source'])) {
+ return $selectBuilder;
+ }
+ $joins = [];
+ $filters = $selectBuilder->getFilters();
+
+ $sourceAlias = $this->nameResolver->getAlias($queryConfig['source']);
+
+ foreach ($queryConfig['source']['link-source'] as $join) {
+ $joinAlias = $this->nameResolver->getAlias($join);
+
+ $joins[$joinAlias] = [
+ 'link-type' => isset($join['link-type']) ? $join['link-type'] : 'left',
+ 'table' => [
+ $joinAlias => $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($join)),
+ ],
+ 'condition' => $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['using'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ];
+ if (isset($join['filter'])) {
+ $filters = array_merge(
+ $filters,
+ [
+ $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['filter'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ]
+ );
+ }
+ $columns = $this->columnsResolver->getColumns($selectBuilder, isset($join['attribute']) ? $join : []);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ }
+ $selectBuilder->setFilters($filters);
+ $selectBuilder->setJoins(array_merge($selectBuilder->getJoins(), $joins));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
new file mode 100644
index 0000000000000..e6474d4c5dc6d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
@@ -0,0 +1,100 @@
+nameResolver = $nameResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Set columns list to SelectBuilder
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $entityConfig
+ * @return array
+ */
+ public function getColumns(SelectBuilder $selectBuilder, $entityConfig)
+ {
+ if (!isset($entityConfig['attribute'])) {
+ return [];
+ }
+ $group = [];
+ $columns = $selectBuilder->getColumns();
+ foreach ($entityConfig['attribute'] as $attributeData) {
+ $columnAlias = $this->nameResolver->getAlias($attributeData);
+ $tableAlias = $this->nameResolver->getAlias($entityConfig);
+ $columnName = $this->nameResolver->getName($attributeData);
+ if (isset($attributeData['function'])) {
+ $prefix = '';
+ if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) {
+ $prefix = ' DISTINCT ';
+ }
+ $expression = new ColumnValueExpression(
+ strtoupper($attributeData['function']) . '(' . $prefix
+ . $this->getConnection()->quoteIdentifier($tableAlias . '.' . $columnName)
+ . ')'
+ );
+ } else {
+ $expression = $tableAlias . '.' . $columnName;
+ }
+ $columns[$columnAlias] = $expression;
+ if (isset($attributeData['group'])) {
+ $group[$columnAlias] = $expression;
+ }
+ }
+ $selectBuilder->setGroup(array_merge($selectBuilder->getGroup(), $group));
+ return $columns;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
new file mode 100644
index 0000000000000..773b96959e794
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
@@ -0,0 +1,166 @@
+ '%1$s = %2$s',
+ 'neq' => '%1$s != %2$s',
+ 'like' => '%1$s LIKE %2$s',
+ 'nlike' => '%1$s NOT LIKE %2$s',
+ 'in' => '%1$s IN(%2$s)',
+ 'nin' => '%1$s NOT IN(%2$s)',
+ 'notnull' => '%1$s IS NOT NULL',
+ 'null' => '%1$s IS NULL',
+ 'gt' => '%1$s > %2$s',
+ 'lt' => '%1$s < %2$s',
+ 'gteq' => '%1$s >= %2$s',
+ 'lteq' => '%1$s <= %2$s',
+ 'finset' => 'FIND_IN_SET(%2$s, %1$s)'
+ ];
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * ConditionResolver constructor.
+ * @param ResourceConnection $resourceConnection
+ */
+ public function __construct(
+ ResourceConnection $resourceConnection
+ ) {
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Returns value for condition
+ *
+ * @param string $condition
+ * @param string $referencedEntity
+ * @return mixed|null|string|\Zend_Db_Expr
+ */
+ private function getValue($condition, $referencedEntity)
+ {
+ $value = null;
+ $argument = isset($condition['_value']) ? $condition['_value'] : null;
+ if (!isset($condition['type'])) {
+ $condition['type'] = 'value';
+ }
+
+ switch ($condition['type']) {
+ case "value":
+ $value = $this->getConnection()->quote($argument);
+ break;
+ case "variable":
+ $value = new Expression($argument);
+ break;
+ case "identifier":
+ $value = $this->getConnection()->quoteIdentifier(
+ $referencedEntity ? $referencedEntity . '.' . $argument : $argument
+ );
+ break;
+ }
+ return $value;
+ }
+
+ /**
+ * Returns condition for WHERE
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param string $tableName
+ * @param array $condition
+ * @param null|string $referencedEntity
+ * @return string
+ */
+ private function getCondition(SelectBuilder $selectBuilder, $tableName, $condition, $referencedEntity = null)
+ {
+ $columns = $selectBuilder->getColumns();
+ if (isset($columns[$condition['attribute']])
+ && $columns[$condition['attribute']] instanceof Expression
+ ) {
+ $expression = $columns[$condition['attribute']];
+ } else {
+ $expression = $this->getConnection()->quoteIdentifier($tableName . '.' . $condition['attribute']);
+ }
+ return sprintf(
+ $this->conditionMap[$condition['operator']],
+ $expression,
+ $this->getValue($condition, $referencedEntity)
+ );
+ }
+
+ /**
+ * Build WHERE condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $filterConfig
+ * @param string $aliasName
+ * @param null|string $referencedAlias
+ * @return array
+ */
+ public function getFilter(SelectBuilder $selectBuilder, $filterConfig, $aliasName, $referencedAlias = null)
+ {
+ $filtersParts = [];
+ foreach ($filterConfig as $filter) {
+ $glue = $filter['glue'];
+ $parts = [];
+ foreach ($filter['condition'] as $condition) {
+ if (isset($condition['type']) && $condition['type'] == 'variable') {
+ $selectBuilder->setParams(array_merge($selectBuilder->getParams(), [$condition['_value']]));
+ }
+ $parts[] = $this->getCondition(
+ $selectBuilder,
+ $aliasName,
+ $condition,
+ $referencedAlias
+ );
+ }
+ if (isset($filter['filter'])) {
+ $parts[] = '(' . $this->getFilter(
+ $selectBuilder,
+ $filter['filter'],
+ $aliasName,
+ $referencedAlias
+ ) . ')';
+ }
+ $filtersParts[] = '(' . implode(' ' . strtoupper($glue) . ' ', $parts) . ')';
+ }
+ return implode(' OR ', $filtersParts);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
new file mode 100644
index 0000000000000..c9543002eb272
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
@@ -0,0 +1,40 @@
+getName($elementConfig);
+ if (isset($elementConfig['alias'])) {
+ $alias = $elementConfig['alias'];
+ }
+ return $alias;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
new file mode 100644
index 0000000000000..21a641f0a71c7
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
@@ -0,0 +1,64 @@
+connectionFactory = $connectionFactory;
+ $this->queryFactory = $queryFactory;
+ }
+
+ /**
+ * Tries to do query for provided report with limit 0 and return error information if it failed
+ *
+ * @param string $name
+ * @param SearchCriteriaInterface $criteria
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function validate($name, SearchCriteriaInterface $criteria = null)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $query->getSelect()->limit(0);
+ try {
+ $connection->query($query->getSelect());
+ } catch (\Zend_Db_Statement_Exception $e) {
+ return [$name, $e->getMessage()];
+ }
+
+ return [];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
new file mode 100644
index 0000000000000..4e5a1940773b1
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
@@ -0,0 +1,289 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get join condition
+ *
+ * @return array
+ */
+ public function getJoins()
+ {
+ return $this->joins;
+ }
+
+ /**
+ * Set joins conditions
+ *
+ * @param array $joins
+ * @return void
+ */
+ public function setJoins($joins)
+ {
+ $this->joins = $joins;
+ }
+
+ /**
+ * Get connection name
+ *
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * Set connection name
+ *
+ * @param string $connectionName
+ * @return void
+ */
+ public function setConnectionName($connectionName)
+ {
+ $this->connectionName = $connectionName;
+ }
+
+ /**
+ * Get columns
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ * @return void
+ */
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+ }
+
+ /**
+ * Get filters
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+
+ /**
+ * Set filters
+ *
+ * @param array $filters
+ * @return void
+ */
+ public function setFilters($filters)
+ {
+ $this->filters = $filters;
+ }
+
+ /**
+ * Get from condition
+ *
+ * @return array
+ */
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ /**
+ * Set from condition
+ *
+ * @param array $from
+ * @return void
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+ }
+
+ /**
+ * Process JOIN conditions
+ *
+ * @param Select $select
+ * @param array $joinConfig
+ * @return Select
+ */
+ private function processJoin(Select $select, $joinConfig)
+ {
+ switch ($joinConfig['link-type']) {
+ case 'left':
+ $select->joinLeft($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'inner':
+ $select->joinInner($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'right':
+ $select->joinRight($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ }
+ return $select;
+ }
+
+ /**
+ * Creates Select object
+ *
+ * @return Select
+ */
+ public function create()
+ {
+ $connection = $this->resourceConnection->getConnection($this->getConnectionName());
+ $select = $connection->select();
+ $select->from($this->getFrom(), []);
+ $select->columns($this->getColumns());
+ foreach ($this->getFilters() as $filter) {
+ $select->where($filter);
+ }
+ foreach ($this->getJoins() as $joinConfig) {
+ $select = $this->processJoin($select, $joinConfig);
+ }
+ if (!empty($this->getGroup())) {
+ $select->group(implode(', ', $this->getGroup()));
+ }
+ return $select;
+ }
+
+ /**
+ * Returns group
+ *
+ * @return array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Set group
+ *
+ * @param array $group
+ * @return void
+ */
+ public function setGroup($group)
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * Get parameters
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Set parameters
+ *
+ * @param array $params
+ * @return void
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * Get having condition
+ *
+ * @return array
+ */
+ public function getHaving()
+ {
+ return $this->having;
+ }
+
+ /**
+ * Set having condition
+ *
+ * @param array $having
+ * @return void
+ */
+ public function setHaving($having)
+ {
+ $this->having = $having;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
new file mode 100644
index 0000000000000..1d88d4618efc5
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
@@ -0,0 +1,43 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Create class instance with specified parameters
+ *
+ * @param array $data
+ * @return SelectBuilder
+ */
+ public function create(array $data = [])
+ {
+ return $this->objectManager->create(SelectBuilder::class, $data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/IteratorFactory.php b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
new file mode 100644
index 0000000000000..a196cef8b66dc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
@@ -0,0 +1,61 @@
+objectManager = $objectManager;
+ $this->defaultIteratorName = $defaultIteratorName;
+ }
+
+ /**
+ * Creates instance of the result iterator with the query result as an input
+ * Result iterator can be changed through report configuration
+ *
+ * < ...
+ *
+ * Uses IteratorIterator by default
+ *
+ * @param \Traversable $result
+ * @param string|null $iteratorName
+ * @return \IteratorIterator
+ */
+ public function create(\Traversable $result, $iteratorName = null)
+ {
+ return $this->objectManager->create(
+ $iteratorName ?: $this->defaultIteratorName,
+ [
+ 'iterator' => $result
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php
new file mode 100644
index 0000000000000..46ec2fb494183
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Query.php
@@ -0,0 +1,96 @@
+select = $select;
+ $this->connectionName = $connectionName;
+ $this->selectHydrator = $selectHydrator;
+ $this->config = $config;
+ }
+
+ /**
+ * @return Select
+ */
+ public function getSelect()
+ {
+ return $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Specify data which should be serialized to JSON
+ * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
+ * @return mixed data which can be serialized by json_encode ,
+ * which is a value of any type other than a resource.
+ * @since 5.4.0
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'connectionName' => $this->getConnectionName(),
+ 'select_parts' => $this->selectHydrator->extract($this->getSelect()),
+ 'config' => $this->getConfig()
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/QueryFactory.php b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
new file mode 100644
index 0000000000000..8ed7e767b28b3
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
@@ -0,0 +1,142 @@
+config = $config;
+ $this->selectBuilderFactory = $selectBuilderFactory;
+ $this->assemblers = $assemblers;
+ $this->queryCache = $queryCache;
+ $this->objectManager = $objectManager;
+ $this->selectHydrator = $selectHydrator;
+ }
+
+ /**
+ * Returns query connection name according to configuration
+ *
+ * @param string $queryConfig
+ * @return string
+ */
+ private function getQueryConnectionName($queryConfig)
+ {
+ $connectionName = 'default';
+ if (isset($queryConfig['connection'])) {
+ $connectionName = $queryConfig['connection'];
+ }
+ return $connectionName;
+ }
+
+ /**
+ * Create query according to configuration settings
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ private function constructQuery($queryName)
+ {
+ $queryConfig = $this->config->get($queryName);
+ $selectBuilder = $this->selectBuilderFactory->create();
+ $selectBuilder->setConnectionName($this->getQueryConnectionName($queryConfig));
+ foreach ($this->assemblers as $assembler) {
+ $selectBuilder = $assembler->assemble($selectBuilder, $queryConfig);
+ }
+ $select = $selectBuilder->create();
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $select,
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $selectBuilder->getConnectionName(),
+ 'config' => $queryConfig
+ ]
+ );
+ }
+
+ /**
+ * Creates query by name
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ public function create($queryName)
+ {
+ $cached = $this->queryCache->load($queryName);
+ if ($cached) {
+ $queryData = json_decode($cached, true);
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $this->selectHydrator->recreate($queryData['select_parts']),
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $queryData['connectionName'],
+ 'config' => $queryData['config']
+ ]
+ );
+ }
+ $query = $this->constructQuery($queryName);
+ $this->queryCache->save(json_encode($query), $queryName);
+ return $query;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
new file mode 100644
index 0000000000000..3ebe5941108bc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
@@ -0,0 +1,76 @@
+queryFactory = $queryFactory;
+ $this->connectionFactory = $connectionFactory;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * Returns custom iterator name for report
+ * Null for default
+ *
+ * @param Query $query
+ * @return string|null
+ */
+ private function getIteratorName(Query $query)
+ {
+ $config = $query->getConfig();
+ return isset($config['iterator']) ? $config['iterator'] : null;
+ }
+
+ /**
+ * Returns report data by name and criteria
+ *
+ * @param string $name
+ * @return \IteratorIterator
+ */
+ public function getReport($name)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $statement = $connection->query($query->getSelect());
+ return $this->iteratorFactory->create($statement, $this->getIteratorName($query));
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/SelectHydrator.php b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
new file mode 100644
index 0000000000000..02cde2fd0598d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
@@ -0,0 +1,143 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ $this->selectParts = $selectParts;
+ }
+
+ /**
+ * @return array
+ */
+ private function getSelectParts()
+ {
+ return array_merge($this->predefinedSelectParts, $this->selectParts);
+ }
+
+ /**
+ * Extracts Select metadata parts
+ *
+ * @param Select $select
+ * @return array
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function extract(Select $select)
+ {
+ $parts = [];
+ foreach ($this->getSelectParts() as $partName) {
+ $parts[$partName] = $select->getPart($partName);
+ }
+ return $parts;
+ }
+
+ /**
+ * @param array $selectParts
+ * @return Select
+ */
+ public function recreate(array $selectParts)
+ {
+ $select = $this->resourceConnection->getConnection()->select();
+
+ $select = $this->processColumns($select, $selectParts);
+
+ foreach ($selectParts as $partName => $partValue) {
+ $select->setPart($partName, $partValue);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Process COLUMNS part values and add this part into select.
+ *
+ * If each column contains information about select expression
+ * an object with the type of this expression going to be created and assigned to this column.
+ *
+ * @param Select $select
+ * @param array $selectParts
+ * @return Select
+ */
+ private function processColumns(Select $select, array &$selectParts)
+ {
+ if (!empty($selectParts[Select::COLUMNS]) && is_array($selectParts[Select::COLUMNS])) {
+ $part = [];
+
+ foreach ($selectParts[Select::COLUMNS] as $columnEntry) {
+ list($correlationName, $column, $alias) = $columnEntry;
+ if (is_array($column) && !empty($column['class'])) {
+ $expression = $this->objectManager->create(
+ $column['class'],
+ isset($column['arguments']) ? $column['arguments'] : []
+ );
+ $part[] = [$correlationName, $expression, $alias];
+ } else {
+ $part[] = $columnEntry;
+ }
+ }
+
+ $select->setPart(Select::COLUMNS, $part);
+ unset($selectParts[Select::COLUMNS]);
+ }
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Analytics/Setup/InstallData.php b/app/code/Magento/Analytics/Setup/InstallData.php
new file mode 100644
index 0000000000000..aaa619bbb0caa
--- /dev/null
+++ b/app/code/Magento/Analytics/Setup/InstallData.php
@@ -0,0 +1,53 @@
+getConnection()->insertMultiple(
+ $setup->getTable('core_config_data'),
+ [
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => 'analytics/subscription/enabled',
+ 'value' => 1
+ ],
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => SubscriptionHandler::CRON_STRING_PATH,
+ 'value' => join(' ', SubscriptionHandler::CRON_EXPR_ARRAY)
+ ]
+ ]
+ );
+
+ $setup->getConnection()->insert(
+ $setup->getTable('flag'),
+ [
+ 'flag_code' => SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE,
+ 'state' => 0,
+ 'flag_data' => 24,
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
new file mode 100644
index 0000000000000..cbf06264096ac
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->additionalComment = $objectManager->getObject(
+ AdditionalComment::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getLabel')
+ ->willReturn('Comment label');
+ $html = $this->additionalComment->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegexp(
+ "/Comment label/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
new file mode 100644
index 0000000000000..a652cf6b3d548
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
@@ -0,0 +1,81 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->setMethods(['getLocaleDate'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->timeZoneMock = $this->getMockBuilder(TimezoneInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock->expects($this->any())
+ ->method('getLocaleDate')
+ ->willReturn($this->timeZoneMock);
+
+ $objectManager = new ObjectManager($this);
+ $this->collectionTimeLabel = $objectManager->getObject(
+ CollectionTimeLabel::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $timeZone = "America/New_York";
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->timeZoneMock->expects($this->once())
+ ->method('getConfigTimezone')
+ ->willReturn($timeZone);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Eastern Standard Time (America/New_York)');
+ $this->assertRegexp(
+ "/Eastern Standard Time \(America\/New_York\)/",
+ $this->collectionTimeLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
new file mode 100644
index 0000000000000..09e753e4ac8aa
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
@@ -0,0 +1,85 @@
+subscriptionStatusProviderMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->subscriptionStatusLabel = $objectManager->getObject(
+ SubscriptionStatusLabel::class,
+ [
+ 'context' => $this->contextMock,
+ 'subscriptionStatusProvider' => $this->subscriptionStatusProviderMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->subscriptionStatusProviderMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn('Enabled');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Subscription status: Enabled');
+ $this->assertRegexp(
+ "/Subscription status: Enabled/",
+ $this->subscriptionStatusLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
new file mode 100644
index 0000000000000..abce48c36c86a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel', 'getHint'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->vertical = $objectManager->getObject(
+ Vertical::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getHint')
+ ->willReturn('New hint');
+ $html = $this->vertical->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegExp(
+ "/New hint/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
new file mode 100644
index 0000000000000..6f613cdc4d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
@@ -0,0 +1,84 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->signUpController = $this->objectManagerHelper->getObject(
+ SignUp::class,
+ [
+ 'config' => $this->configMock,
+ 'resultRedirectFactory' => $this->resultRedirectFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $urlBIEssentialsConfigPath = 'analytics/url/bi_essentials';
+ $this->configMock->expects($this->once())
+ ->method('getValue')
+ ->with($urlBIEssentialsConfigPath)
+ ->willReturn('value');
+ $this->resultRedirectFactoryMock->expects($this->once())->method('create')->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->once())->method('setUrl')->with('value')->willReturnSelf();
+ $this->assertEquals($this->redirectMock, $this->signUpController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
new file mode 100644
index 0000000000000..4f54ce5059965
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
@@ -0,0 +1,185 @@
+reportUrlProviderMock = $this->getMockBuilder(ReportUrlProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->showController = $this->objectManagerHelper->getObject(
+ Show::class,
+ [
+ 'reportUrlProvider' => $this->reportUrlProviderMock,
+ 'resultFactory' => $this->resultFactoryMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $otpUrl = 'http://example.com?otp=15vbjcfdvd15645';
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willReturn($otpUrl);
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setUrl')
+ ->with($otpUrl)
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @dataProvider executeWithExceptionDataProvider
+ *
+ * @param \Exception $exception
+ */
+ public function testExecuteWithException(\Exception $exception)
+ {
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ if ($exception instanceof LocalizedException) {
+ $message = $exception->getMessage();
+ } else {
+ $message = __('Sorry, there has been an error processing your request. Please try again later.');
+ }
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message)
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeWithExceptionDataProvider()
+ {
+ return [
+ 'ExecuteWithLocalizedException' => [new LocalizedException(__('TestMessage'))],
+ 'ExecuteWithException' => [new \Exception('TestMessage')],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithSubscriptionUpdateException()
+ {
+ $exception = new SubscriptionUpdateException(__('TestMessage'));
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addNoticeMessage')
+ ->with($exception->getMessage())
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
new file mode 100644
index 0000000000000..17c485a8df230
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
@@ -0,0 +1,159 @@
+resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultRedirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->retryController = $this->objectManagerHelper->getObject(
+ Retry::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @dataProvider executeExceptionsDataProvider
+ *
+ * @param \Exception $exception
+ * @param Phrase $message
+ */
+ public function testExecuteWithException(\Exception $exception, Phrase $message)
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message);
+
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function executeExceptionsDataProvider()
+ {
+ return [
+ [new LocalizedException(__('TestMessage')), __('TestMessage')],
+ [
+ new \Exception('TestMessage'),
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
new file mode 100644
index 0000000000000..81c57d79033c8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
@@ -0,0 +1,91 @@
+exportDataHandlerMock = $this->getMockBuilder(ExportDataHandlerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectData = $this->objectManagerHelper->getObject(
+ CollectData::class,
+ [
+ 'exportDataHandler' => $this->exportDataHandlerMock,
+ 'subscriptionStatus' => $this->subscriptionStatusMock,
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ * @return void
+ * @dataProvider executeDataProvider
+ */
+ public function testExecute($status)
+ {
+ $this->subscriptionStatusMock
+ ->expects($this->once())
+ ->method('getStatus')
+ ->with()
+ ->willReturn($status);
+ $this->exportDataHandlerMock
+ ->expects(($status === SubscriptionStatusProvider::ENABLED) ? $this->once() : $this->never())
+ ->method('prepareExportData')
+ ->with();
+
+ $this->assertTrue($this->collectData->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeDataProvider()
+ {
+ return [
+ 'Subscription is enabled' => [SubscriptionStatusProvider::ENABLED],
+ 'Subscription is disabled' => [SubscriptionStatusProvider::DISABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
new file mode 100644
index 0000000000000..959a11f9e1058
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
@@ -0,0 +1,133 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUp = new SignUp(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->flagManagerMock,
+ $this->reinitableConfigMock
+ );
+ }
+
+ public function testExecute()
+ {
+ $attemptsCount = 10;
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+
+ $attemptsCount -= 1;
+ $this->flagManagerMock->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('signUp')
+ ->willReturn(true);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertTrue($this->signUp->execute());
+ }
+
+ public function testExecuteFlagNotExist()
+ {
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(null);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ public function testExecuteZeroAttempts()
+ {
+ $attemptsCount = 0;
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ /**
+ * Add assertions for method deleteAnalyticsCronExpr.
+ *
+ * @return void
+ */
+ private function addDeleteAnalyticsCronExprAsserts()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(SubscriptionHandler::CRON_STRING_PATH)
+ ->willReturn(true);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
new file mode 100644
index 0000000000000..ede53d8783a7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
@@ -0,0 +1,214 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->update = new Update(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->reinitableConfigMock,
+ $this->flagManagerMock,
+ $this->analyticsTokenMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithoutToken()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(10);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * @param bool $isExecuted
+ */
+ private function addFinalOutputAsserts(bool $isExecuted = true)
+ {
+ $this->flagManagerMock
+ ->expects($this->exactly(2 * $isExecuted))
+ ->method('deleteFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE]
+ );
+ $this->configWriterMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('delete')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfigMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('reinit')
+ ->with();
+ }
+
+ /**
+ * @param $counterData
+ * @return void
+ * @dataProvider executeWithEmptyReverseCounterDataProvider
+ */
+ public function testExecuteWithEmptyReverseCounter($counterData)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($counterData);
+ $this->connectorMock
+ ->expects($this->never())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * Provides empty states of the reverse counter.
+ *
+ * @return array
+ */
+ public function executeWithEmptyReverseCounterDataProvider()
+ {
+ return [
+ [null],
+ [0]
+ ];
+ }
+
+ /**
+ * @param int $reverseCount
+ * @param bool $commandResult
+ * @param bool $finalConditionsIsExpected
+ * @param bool $functionResult
+ * @return void
+ * @dataProvider executeRegularScenarioDataProvider
+ */
+ public function testExecuteRegularScenario(
+ int $reverseCount,
+ bool $commandResult,
+ bool $finalConditionsIsExpected,
+ bool $functionResult
+ ) {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($reverseCount);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn($commandResult);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts($finalConditionsIsExpected);
+ $this->assertSame($functionResult, $this->update->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeRegularScenarioDataProvider()
+ {
+ return [
+ 'The last attempt with command execution result False' => [
+ 'Reverse count' => 1,
+ 'Command result' => false,
+ 'Executed final output conditions' => true,
+ 'Function result' => false,
+ ],
+ 'Not the last attempt with command execution result False' => [
+ 'Reverse count' => 10,
+ 'Command result' => false,
+ 'Executed final output conditions' => false,
+ 'Function result' => false,
+ ],
+ 'Command execution result True' => [
+ 'Reverse count' => 10,
+ 'Command result' => true,
+ 'Executed final output conditions' => true,
+ 'Function result' => true,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
new file mode 100644
index 0000000000000..57315543bc32d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
@@ -0,0 +1,129 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->tokenModel = $this->objectManagerHelper->getObject(
+ AnalyticsToken::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'config' => $this->configMock,
+ 'configWriter' => $this->configWriterMock,
+ 'tokenPath' => $this->tokenPath,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testStoreToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->tokenPath, $value);
+
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+
+ $this->assertTrue($this->tokenModel->storeToken($value));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn($value);
+
+ $this->assertSame($value, $this->tokenModel->getToken());
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsTokenExist()
+ {
+ $this->assertFalse($this->tokenModel->isTokenExist());
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn('0000');
+ $this->assertTrue($this->tokenModel->isTokenExist());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
new file mode 100644
index 0000000000000..f5f721c038c57
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
@@ -0,0 +1,178 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionUpdateHandler = $this->objectManagerHelper->getObject(
+ SubscriptionUpdateHandler::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenDoesNotExist()
+ {
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate('http://store.com'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenAndPreviousBaseUrlExist()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenExistAndWithoutPreviousBaseUrl()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->exactly(2))
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url],
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
new file mode 100644
index 0000000000000..071b96111ac8b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
@@ -0,0 +1,111 @@
+configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectionTime = $this->objectManagerHelper->getObject(
+ CollectionTime::class,
+ [
+ 'configWriter' => $this->configWriterMock,
+ '_logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSave()
+ {
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']));
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->collectionTime->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWrongValue()
+ {
+ $this->collectionTime->setData('value', '00,01');
+ $this->collectionTime->afterSave();
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWithLocalizedException()
+ {
+ $exception = new \Exception('Test message');
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']))
+ ->willThrowException($exception);
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+ $this->collectionTime->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
new file mode 100644
index 0000000000000..82aa4dc72dfe0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
@@ -0,0 +1,152 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionHandler = $this->objectManagerHelper->getObject(
+ SubscriptionHandler::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ 'attemptsInitValue' => $this->attemptsInitValue,
+ 'analyticsToken' => $this->tokenMock,
+ ]
+ );
+ }
+
+ public function testProcessEnabledTokenExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessEnabledTokenDoesNotExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *");
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessDisabledTokenDoesNotExist()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+
+ public function testProcessDisabledTokenExists()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('deleteFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
new file mode 100644
index 0000000000000..eea3193258bc6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
@@ -0,0 +1,184 @@
+subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->enabledModel = $this->objectManagerHelper->getObject(
+ Enabled::class,
+ [
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ '_logger' => $this->loggerMock,
+ 'config' => $this->configMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessEnabled()
+ {
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessDisabled()
+ {
+ $this->enabledModel->setData('value', $this->valueDisabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueDisabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessValueNotChanged()
+ {
+ $this->enabledModel->setData('value', null);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(null);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testExecuteAfterSaveFailedWithLocalizedException()
+ {
+ $exception = new \Exception('Message');
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+
+ $this->enabledModel->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
new file mode 100644
index 0000000000000..6fe7d0aa93998
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
@@ -0,0 +1,59 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testBeforeSaveSuccess()
+ {
+ $this->subject->setValue('Apps and Games');
+
+ $this->assertInstanceOf(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class,
+ $this->subject->beforeSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testBeforeSaveFailedWithLocalizedException()
+ {
+ $this->subject->setValue('');
+
+ $this->subject->beforeSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
new file mode 100644
index 0000000000000..0b7f4870dbac8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
@@ -0,0 +1,142 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->mapper = $this->objectManagerHelper->getObject(Mapper::class);
+ }
+
+ /**
+ * @param array $configData
+ * @param array $resultData
+ * @return void
+ *
+ * @dataProvider executingDataProvider
+ */
+ public function testExecution($configData, $resultData)
+ {
+ $this->assertSame($resultData, $this->mapper->execute($configData));
+ }
+
+ /**
+ * @return array
+ */
+ public function executingDataProvider()
+ {
+ return [
+ 'wrongConfig' => [
+ ['config' => ['files']],
+ []
+ ],
+ 'validConfigWithFileNodes' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [[]]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => []
+ ]
+ ],
+ ],
+ 'validConfigWithProvidersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [0 => []]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => ['parameters' => []]
+ ]
+ ]
+ ],
+ ],
+ 'validConfigWithParametersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [
+ 0 => [
+ 'parameters' => [
+ 0 => ['name' => ['reportName']]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => [
+ 'parameters' => [
+ 'name' => 'reportName'
+ ]
+ ]
+ ]
+ ]
+ ],
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
new file mode 100644
index 0000000000000..6aa9c7ef3106c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
@@ -0,0 +1,108 @@
+mapperMock = $this->getMockBuilder(Mapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerXmlMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerDbMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reader = $this->objectManagerHelper->getObject(
+ Reader::class,
+ [
+ 'mapper' => $this->mapperMock,
+ 'readers' => [
+ $this->readerXmlMock,
+ $this->readerDbMock,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testRead()
+ {
+ $scope = 'store';
+ $xmlReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node4']]
+ ];
+ $dbReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node3']]
+ ];
+ $mapperResult = ['node2' => ['node3', 'node4']];
+
+ $this->readerXmlMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($xmlReaderResult);
+
+ $this->readerDbMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($dbReaderResult);
+
+ $this->mapperMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with(array_merge_recursive($xmlReaderResult, $dbReaderResult))
+ ->willReturn($mapperResult);
+
+ $this->assertSame($mapperResult, $this->reader->read($scope));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
new file mode 100644
index 0000000000000..c13205d34f25b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
@@ -0,0 +1,60 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Source\Vertical::class,
+ [
+ 'verticals' => [
+ 'Apps and Games',
+ 'Athletic/Sporting Goods',
+ 'Art and Design'
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testToOptionArray()
+ {
+ $expectedOptionsArray = [
+ ['value' => '', 'label' => __('--Please Select--')],
+ ['value' => 'Apps and Games', 'label' => __('Apps and Games')],
+ ['value' => 'Athletic/Sporting Goods', 'label' => __('Athletic/Sporting Goods')],
+ ['value' => 'Art and Design', 'label' => __('Art and Design')]
+ ];
+
+ $this->assertEquals(
+ $expectedOptionsArray,
+ $this->subject->toOptionArray()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
new file mode 100644
index 0000000000000..8739219ebdf09
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
@@ -0,0 +1,68 @@
+dataInterfaceMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataInterfaceMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGet()
+ {
+ $key = 'configKey';
+ $defaultValue = 'mock';
+ $configValue = 'emptyString';
+
+ $this->dataInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($key, $defaultValue)
+ ->willReturn($configValue);
+
+ $this->assertSame($configValue, $this->config->get($key, $defaultValue));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
new file mode 100644
index 0000000000000..f8f3919b2489e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
@@ -0,0 +1,218 @@
+curlMock = $this->getMockBuilder(
+ \Magento\Framework\HTTP\Adapter\Curl::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock = $this->getMockBuilder(CurlFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->curlMock);
+
+ $this->responseFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ResponseFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->converterMock = $this->createJsonConverter();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\Http\Client\Curl::class,
+ [
+ 'curlFactory' => $this->curlFactoryMock,
+ 'responseFactory' => $this->responseFactoryMock,
+ 'converter' => $this->converterMock,
+ 'logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ public function getTestData()
+ {
+ return [
+ [
+ 'data' => [
+ 'version' => '1.1',
+ 'body'=> ['name' => 'value'],
+ 'url' => 'http://www.mystore.com',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestSuccess(array $data)
+ {
+ $responseString = 'This is response.';
+ $response = new \Zend_Http_Response(201, [], $responseString);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read')
+ ->willReturn($responseString);
+ $this->curlMock->expects($this->any())
+ ->method('getErrno')
+ ->willReturn(0);
+
+ $this->responseFactoryMock->expects($this->any())
+ ->method('create')
+ ->with($responseString)
+ ->willReturn($response);
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestError(array $data)
+ {
+ $response = new \Zend_Http_Response(0, []);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read');
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getErrno')
+ ->willReturn(1);
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getError')
+ ->willReturn('CURL error.');
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with(
+ new \Exception(
+ 'MBI service CURL connection error #1: CURL error.'
+ )
+ );
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createJsonConverter()
+ {
+ $converterMock = $this->getMockBuilder(ConverterInterface::class)
+ ->getMockForAbstractClass();
+ $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) {
+ return json_encode($value);
+ });
+ $converterMock->expects($this->any())
+ ->method('getContentTypeHeader')
+ ->willReturn(JsonConverter::CONTENT_TYPE_HEADER);
+ return $converterMock;
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
new file mode 100644
index 0000000000000..60a19f3d5079e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
@@ -0,0 +1,34 @@
+assertEquals(JsonConverter::CONTENT_TYPE_HEADER, $converter->getContentTypeHeader());
+ }
+
+ public function testConvertBody()
+ {
+ $body = '{"token": "secret-token"}';
+ $converter = new JsonConverter();
+ $this->assertEquals(json_decode($body, 1), $converter->fromBody($body));
+ }
+
+ public function testConvertData()
+ {
+ $data = ["token" => "secret-token"];
+ $converter = new JsonConverter();
+ $this->assertEquals(json_encode($data), $converter->toBody($data));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
new file mode 100644
index 0000000000000..7c3c484843285
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
@@ -0,0 +1,39 @@
+ 'testValue'];
+ $response = new \Zend_Http_Response(201, [], json_encode($expectedBody));
+ $responseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $responseHandlerMock->expects($this->once())
+ ->method('handleResponse')
+ ->with($expectedBody)
+ ->willReturn(true);
+ $notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $notFoundResponseHandlerMock->expects($this->never())->method('handleResponse');
+ $responseResolver = new ResponseResolver(
+ new JsonConverter(),
+ [
+ 201 => $responseHandlerMock,
+ 404 => $notFoundResponseHandlerMock,
+ ]
+ );
+ $this->assertTrue($responseResolver->getResult($response));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
new file mode 100644
index 0000000000000..cee3877631c2e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
@@ -0,0 +1,107 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $successHandler = $this->getMockBuilder(\Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $successHandler->method('handleResponse')
+ ->willReturn(true);
+
+ $this->notifyDataChangedCommand = new NotifyDataChangedCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ new ResponseResolver(new JsonConverter(), [201 => $successHandler]),
+ $this->loggerMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::POST,
+ $configVal,
+ ['access-token' => $token, 'url' => $configVal]
+ )->willReturn(new \Zend_Http_Response(201, []));
+ $this->assertTrue($this->notifyDataChangedCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+ $this->assertFalse($this->notifyDataChangedCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
new file mode 100644
index 0000000000000..8a3f4efb15cf4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
@@ -0,0 +1,187 @@
+loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = new OTPRequest(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->responseResolverMock,
+ $this->loggerMock
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'otp' => 'thisisotp',
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['access-token' => 'thisisaccesstoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallSuccess()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(201, []));
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn($data['otp']);
+
+ $this->assertEquals(
+ $data['otp'],
+ $this->subject->call()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoAccessToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+
+ $this->assertFalse($this->subject->call());
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoOtp()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(0, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(false);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse($this->subject->call());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
new file mode 100644
index 0000000000000..0ff36cca5db2d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
@@ -0,0 +1,22 @@
+assertFalse($OTPHandler->handleResponse([]));
+ $expectedOtp = 123;
+ $this->assertEquals($expectedOtp, $OTPHandler->handleResponse(['otp' => $expectedOtp]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
new file mode 100644
index 0000000000000..707003149bcfd
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
@@ -0,0 +1,36 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with(null);
+ $subscriptionHandler = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider->method('getStatus')->willReturn(SubscriptionStatusProvider::ENABLED);
+ $reSignUpHandler = new ReSignUp($analyticsToken, $subscriptionHandler, $subscriptionStatusProvider);
+ $this->assertFalse($reSignUpHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
new file mode 100644
index 0000000000000..81711cfc56950
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
@@ -0,0 +1,30 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with($accessToken);
+ $signUpHandler = new SignUp($analyticsToken, new JsonConverter());
+ $this->assertFalse($signUpHandler->handleResponse([]));
+ $this->assertEquals($accessToken, $signUpHandler->handleResponse(['access-token' => $accessToken]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
new file mode 100644
index 0000000000000..7779357e8bea7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
@@ -0,0 +1,20 @@
+assertTrue($updateHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
new file mode 100644
index 0000000000000..5593496a957b7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
@@ -0,0 +1,174 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationToken = $this->getMockBuilder(IntegrationToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUpCommand = new SignUpCommand(
+ $this->analyticsTokenMock,
+ $this->integrationManagerMock,
+ $this->configMock,
+ $this->httpClientMock,
+ $this->loggerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+ $this->integrationToken->expects($this->any())
+ ->method('getData')
+ ->with('token')
+ ->willReturn($data['integration-token']);
+ $httpResponse = new \Zend_Http_Response(201, [], '{"access-token": "' . $data['access-token'] . '"}');
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->with($httpResponse)
+ ->willReturn(true);
+ $this->assertTrue($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureCannotGenerateToken()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn(false);
+ $this->integrationManagerMock->expects($this->never())
+ ->method('activateIntegration');
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureResponseIsEmpty()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $httpResponse = new \Zend_Http_Response(0, []);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->willReturn(false);
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'integration-token' => 'thisisintegrationtoken',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['token' => 'thisisintegrationtoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
new file mode 100644
index 0000000000000..47253a13530e5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
@@ -0,0 +1,143 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->updateCommand = new UpdateCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->flagManagerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $url = "old.localhost.com";
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn($url);
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::PUT,
+ $configVal,
+ [
+ 'url' => $url,
+ 'new-url' => $configVal,
+ 'access-token' => $token
+ ]
+ )->willReturn(new \Zend_Http_Response(200, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(true);
+
+ $this->assertTrue($this->updateCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->assertFalse($this->updateCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
new file mode 100644
index 0000000000000..4414b81cbc183
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
@@ -0,0 +1,70 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->signUpCommandMock = $this->getMockBuilder(SignUpCommand::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->commands = ['signUp' => SignUpCommand::class];
+ $this->connector = new Connector($this->commands, $this->objectManagerMock);
+ }
+
+ public function testExecute()
+ {
+ $commandName = 'signUp';
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with($this->commands[$commandName])
+ ->willReturn($this->signUpCommandMock);
+ $this->signUpCommandMock->expects($this->once())
+ ->method('execute')
+ ->willReturn(true);
+ $this->assertTrue($this->connector->execute($commandName));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NotFoundException
+ */
+ public function testExecuteCommandNotFound()
+ {
+ $commandName = 'register';
+ $this->connector->execute($commandName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
new file mode 100644
index 0000000000000..a896c309b4007
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
@@ -0,0 +1,226 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextFactoryMock = $this->getMockBuilder(EncodedContextFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->key = '';
+ $this->source = '';
+ $this->initializationVectors = [];
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'encodedContextFactory' => $this->encodedContextFactoryMock,
+ 'cipherMethod' => $this->cipherMethod,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncode()
+ {
+ $token = 'some-token-value';
+ $this->source = 'Some text';
+ $this->key = hash('sha256', $token);
+
+ $checkEncodedContext = function ($parameters) {
+ $emptyRequiredParameters =
+ array_diff(['content', 'initializationVector'], array_keys(array_filter($parameters)));
+ if ($emptyRequiredParameters) {
+ return false;
+ }
+
+ $encryptedData = openssl_encrypt(
+ $this->source,
+ $this->cipherMethod,
+ $this->key,
+ OPENSSL_RAW_DATA,
+ $parameters['initializationVector']
+ );
+
+ return ($encryptedData === $parameters['content']);
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback($checkEncodedContext))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncodeUniqueInitializationVector()
+ {
+ $this->source = 'Some text';
+ $token = 'some-token-value';
+
+ $registerInitializationVector = function ($parameters) {
+ if (empty($parameters['initializationVector'])) {
+ return false;
+ }
+
+ $this->initializationVectors[] = $parameters['initializationVector'];
+
+ return true;
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->exactly(2))
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->with($this->callback($registerInitializationVector))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertCount(2, array_unique($this->initializationVectors));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @dataProvider encodeNotValidSourceDataProvider
+ */
+ public function testEncodeNotValidSource($source)
+ {
+ $this->cryptographer->encode($source);
+ }
+
+ /**
+ * @return array
+ */
+ public function encodeNotValidSourceDataProvider()
+ {
+ return [
+ 'Array' => [[]],
+ 'Empty string' => [''],
+ ];
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeNotValidCipherMethod()
+ {
+ $source = 'Some string';
+ $cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'cipherMethod' => 'Wrong-method',
+ ]
+ );
+
+ $cryptographer->encode($source);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeTokenNotValid()
+ {
+ $source = 'Some string';
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn(null);
+
+ $this->cryptographer->encode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
new file mode 100644
index 0000000000000..a1a7c54510681
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
@@ -0,0 +1,61 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string $content
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($content, $initializationVector)
+ {
+ $constructorArguments = [
+ 'content' => $content,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var EncodedContext $encodedContext */
+ $encodedContext = $this->objectManagerHelper->getObject(
+ EncodedContext::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($content, $encodedContext->getContent());
+ $this->assertSame($initializationVector ?: '', $encodedContext->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
new file mode 100644
index 0000000000000..1582c241bf45d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
@@ -0,0 +1,74 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @return void
+ */
+ public function testThatNotifyExecuted()
+ {
+ $expectedResult = true;
+ $notifyCommandName = 'notifyDataChanged';
+ $exportDataHandlerMockObject = $this->createExportDataHandlerMock();
+ $analyticsConnectorMockObject = $this->createAnalyticsConnectorMock();
+ /**
+ * @var $exportDataHandlerNotification ExportDataHandlerNotification
+ */
+ $exportDataHandlerNotification = $this->objectManagerHelper->getObject(
+ ExportDataHandlerNotification::class,
+ [
+ 'exportDataHandler' => $exportDataHandlerMockObject,
+ 'connector' => $analyticsConnectorMockObject,
+ ]
+ );
+ $exportDataHandlerMockObject->expects($this->once())
+ ->method('prepareExportData')
+ ->willReturn($expectedResult);
+ $analyticsConnectorMockObject->expects($this->once())
+ ->method('execute')
+ ->with($notifyCommandName);
+ $this->assertEquals($expectedResult, $exportDataHandlerNotification->prepareExportData());
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createExportDataHandlerMock()
+ {
+ return $this->getMockBuilder(ExportDataHandler::class)->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAnalyticsConnectorMock()
+ {
+ return $this->getMockBuilder(Connector::class)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
new file mode 100644
index 0000000000000..6ffb61d088567
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
@@ -0,0 +1,270 @@
+filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->archiveMock = $this->getMockBuilder(Archive::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->reportWriterMock = $this->getMockBuilder(ReportWriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cryptographerMock = $this->getMockBuilder(Cryptographer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileRecorderMock = $this->getMockBuilder(FileRecorder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->exportDataHandler = $this->objectManagerHelper->getObject(
+ ExportDataHandler::class,
+ [
+ 'filesystem' => $this->filesystemMock,
+ 'archive' => $this->archiveMock,
+ 'reportWriter' => $this->reportWriterMock,
+ 'cryptographer' => $this->cryptographerMock,
+ 'fileRecorder' => $this->fileRecorderMock,
+ 'subdirectoryPath' => $this->subdirectoryPath,
+ 'archiveName' => $this->archiveName,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isArchiveSourceDirectory
+ * @dataProvider prepareExportDataDataProvider
+ */
+ public function testPrepareExportData($isArchiveSourceDirectory)
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archiveRelativePath = $this->subdirectoryPath . $this->archiveName;
+
+ $archiveSource = $isArchiveSourceDirectory ? (__DIR__) : '/tmp/' . $tmpFilesDirectoryPath;
+ $archiveAbsolutePath = '/tmp/' . $archiveRelativePath;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ );
+
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('getAbsolutePath')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ $archiveSource,
+ $archiveSource,
+ $archiveAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('isExist')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ true,
+ true
+ );
+
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(dirname($archiveRelativePath));
+
+ $this->archiveMock
+ ->expects($this->once())
+ ->method('pack')
+ ->with(
+ $archiveSource,
+ $archiveAbsolutePath,
+ $isArchiveSourceDirectory ? true : false
+ );
+
+ $fileContent = 'Some text';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('readFile')
+ ->with($archiveRelativePath)
+ ->willReturn($fileContent);
+
+ $this->cryptographerMock
+ ->expects($this->once())
+ ->method('encode')
+ ->with($fileContent)
+ ->willReturn($this->encodedContextMock);
+
+ $this->fileRecorderMock
+ ->expects($this->once())
+ ->method('recordNewFile')
+ ->with($this->encodedContextMock);
+
+ $this->assertTrue($this->exportDataHandler->prepareExportData());
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareExportDataDataProvider()
+ {
+ return [
+ 'Data source for archive is directory' => [true],
+ 'Data source for archive doesn\'t directory' => [false],
+ ];
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testPrepareExportDataWithLocalizedException()
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archivePath = $this->subdirectoryPath . $this->archiveName;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->exactly(3))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archivePath]
+ );
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('getAbsolutePath')
+ ->with($tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->with($tmpFilesDirectoryPath)
+ ->willReturn(false);
+
+ $this->assertNull($this->exportDataHandler->prepareExportData());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
new file mode 100644
index 0000000000000..da5f6af3ca4e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
@@ -0,0 +1,194 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileInfoManager = $this->objectManagerHelper->getObject(
+ FileInfoManager::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'flagCode' => $this->flagCode,
+ 'encodedParameters' => $this->encodedParameters,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testSave()
+ {
+ $path = 'path/to/file';
+ $initializationVector = openssl_random_pseudo_bytes(16);
+ $parameters = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = base64_encode($parameters[$encodedParameter]);
+ }
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with($this->flagCode, $parameters);
+
+ $this->assertTrue($this->fileInfoManager->save($this->fileInfoMock));
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @dataProvider saveWithLocalizedExceptionDataProvider
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testSaveWithLocalizedException($path, $initializationVector)
+ {
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ $this->fileInfoManager->save($this->fileInfoMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function saveWithLocalizedExceptionDataProvider()
+ {
+ return [
+ 'Empty FileInfo' => [null, null],
+ 'FileInfo without IV' => ['path/to/file', null],
+ ];
+ }
+
+ /**
+ * @dataProvider loadDataProvider
+ * @param array|null $parameters
+ */
+ public function testLoad($parameters)
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with($this->flagCode)
+ ->willReturn($parameters);
+
+ $processedParameters = $parameters ?: [];
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($processedParameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $processedParameters[$encodedParameter] = base64_decode($processedParameters[$encodedParameter]);
+ }
+
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($processedParameters)
+ ->willReturn($this->fileInfoMock);
+
+ $this->assertSame($this->fileInfoMock, $this->fileInfoManager->load());
+ }
+
+ /**
+ * @return array
+ */
+ public function loadDataProvider()
+ {
+ return [
+ 'Empty flag data' => [null],
+ 'Correct flag data' => [[
+ 'path' => 'path/to/file',
+ 'initializationVector' => 'xUJjl54MVke+FvMFSBpRSA==',
+ ]],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
new file mode 100644
index 0000000000000..43ce833f1f03f
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
@@ -0,0 +1,62 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($path, $initializationVector)
+ {
+ $constructorArguments = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var FileInfo $fileInfo */
+ $fileInfo = $this->objectManagerHelper->getObject(
+ FileInfo::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($path ?: '', $fileInfo->getPath());
+ $this->assertSame($initializationVector ?: '', $fileInfo->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Degenerate object' => [null, null],
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
new file mode 100644
index 0000000000000..3c9520bdd995b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
@@ -0,0 +1,209 @@
+fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileRecorder = $this->objectManagerHelper->getObject(
+ FileRecorder::class,
+ [
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'filesystem' => $this->filesystemMock,
+ 'fileSubdirectoryPath' => $this->fileSubdirectoryPath,
+ 'encodedFileName' => $this->encodedFileName,
+ ]
+ );
+ }
+
+ /**
+ * @param string $pathToExistingFile
+ * @dataProvider recordNewFileDataProvider
+ */
+ public function testRecordNewFile($pathToExistingFile)
+ {
+ $content = openssl_random_pseudo_bytes(200);
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::MEDIA)
+ ->willReturn($this->directoryMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getContent')
+ ->with()
+ ->willReturn($content);
+
+ $hashLength = 64;
+ $fileRelativePathPattern = '#' . preg_quote($this->fileSubdirectoryPath, '#')
+ . '.{' . $hashLength . '}/' . preg_quote($this->encodedFileName, '#') . '#';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('writeFile')
+ ->with($this->matchesRegularExpression($fileRelativePathPattern), $content)
+ ->willReturn($this->directoryMock);
+
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('load')
+ ->with()
+ ->willReturn($this->fileInfoMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn('init_vector***');
+
+ /** register file */
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback(
+ function ($parameters) {
+ return !empty($parameters['path']) && ('init_vector***' === $parameters['initializationVector']);
+ }
+ ))
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->fileInfoMock);
+
+ /** remove old file */
+ $this->fileInfoMock
+ ->expects($this->exactly($pathToExistingFile ? 3 : 1))
+ ->method('getPath')
+ ->with()
+ ->willReturn($pathToExistingFile);
+ $directoryName = dirname($pathToExistingFile);
+ if ($directoryName === '.') {
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with($pathToExistingFile);
+ } elseif ($directoryName) {
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('delete')
+ ->withConsecutive(
+ [$pathToExistingFile],
+ [$directoryName]
+ );
+ }
+
+ $this->assertTrue($this->fileRecorder->recordNewFile($this->encodedContextMock));
+ }
+
+ /**
+ * @return array
+ */
+ public function recordNewFileDataProvider()
+ {
+ return [
+ 'File doesn\'t exist' => [''],
+ 'Existing file into subdirectory' => ['dir_name/file.txt'],
+ 'Existing file doesn\'t into subdirectory' => ['file.txt'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
new file mode 100644
index 0000000000000..3076a22c85be4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
@@ -0,0 +1,228 @@
+integrationServiceMock = $this->getMockBuilder(IntegrationServiceInterface::class)
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->oauthServiceMock = $this->getMockBuilder(OauthServiceInterface::class)
+ ->getMock();
+ $this->integrationMock = $this->getMockBuilder(Integration::class)
+ ->disableOriginalConstructor()
+ ->setMethods([
+ 'getId',
+ 'getConsumerId'
+ ])
+ ->getMock();
+ $this->integrationManager = $objectManagerHelper->getObject(
+ IntegrationManager::class,
+ [
+ 'integrationService' => $this->integrationServiceMock,
+ 'oauthService' => $this->oauthServiceMock,
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return array
+ */
+ private function getIntegrationUserData($status)
+ {
+ return [
+ 'name' => 'ma-integration-user',
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testActivateIntegrationSuccess()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturn(100500);
+ $integrationData = $this->getIntegrationUserData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = 100500;
+ $this->configMock->expects($this->exactly(2))
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('update')
+ ->with($integrationData);
+ $this->assertTrue($this->integrationManager->activateIntegration());
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function testActivateIntegrationFailureNoSuchEntity()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(null);
+ $this->configMock->expects($this->once())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->never())
+ ->method('update');
+ $this->integrationManager->activateIntegration();
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenNewIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->at(0))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn(false);
+ $this->oauthServiceMock->expects($this->at(2))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->once())
+ ->method('createAccessToken')
+ ->with(100500, true)
+ ->willReturn(true);
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenExistingIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->once())
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->never())
+ ->method('createAccessToken');
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @return array
+ */
+ public function integrationIdDataProvider()
+ {
+ return [
+ [1],
+ [null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
new file mode 100644
index 0000000000000..c7aa2219d1eee
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
@@ -0,0 +1,166 @@
+linkInterfaceFactoryMock = $this->getMockBuilder(LinkInterfaceFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+ $this->fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManagerInterfaceMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->getMockForAbstractClass();
+ $this->linkInterfaceMock = $this->getMockBuilder(LinkInterface::class)
+ ->getMockForAbstractClass();
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeMock = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->linkProvider = $this->objectManagerHelper->getObject(
+ LinkProvider::class,
+ [
+ 'linkFactory' => $this->linkInterfaceFactoryMock,
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'storeManager' => $this->storeManagerInterfaceMock
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $baseUrl = 'http://magento.local/pub/media/';
+ $fileInfoPath = 'analytics/data.tgz';
+ $fileInitializationVector = 'er312esq23eqq';
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->linkInterfaceFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'initializationVector' => base64_encode($fileInitializationVector),
+ 'url' => $baseUrl . $fileInfoPath
+ ]
+ )
+ ->willReturn($this->linkInterfaceMock);
+ $this->storeManagerInterfaceMock->expects($this->once())
+ ->method('getStore')->willReturn($this->storeMock);
+ $this->storeMock->expects($this->once())
+ ->method('getBaseUrl')
+ ->with(
+ UrlInterface::URL_TYPE_MEDIA
+ )
+ ->willReturn($baseUrl);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get());
+ }
+
+ /**
+ * @param string|null $fileInfoPath
+ * @param string|null $fileInitializationVector
+ *
+ * @dataProvider fileNotReadyDataProvider
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ * @expectedExceptionMessage File is not ready yet.
+ */
+ public function testFileNotReady($fileInfoPath, $fileInitializationVector)
+ {
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoMock->expects($this->once())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->any())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->linkProvider->get();
+ }
+
+ /**
+ * @return array
+ */
+ public function fileNotReadyDataProvider()
+ {
+ return [
+ [null, 'initVector'],
+ ['path', null],
+ ['', 'initVector'],
+ ['path', ''],
+ ['', ''],
+ [null, null]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
new file mode 100644
index 0000000000000..a89e06562383b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
@@ -0,0 +1,147 @@
+subscriptionUpdateHandlerMock = $this->getMockBuilder(SubscriptionUpdateHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configValueMock = $this->getMockBuilder(Value::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isValueChanged', 'getPath', 'getScope', 'getOldValue'])
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->plugin = $this->objectManagerHelper->getObject(
+ BaseUrlConfigPlugin::class,
+ [
+ 'subscriptionUpdateHandler' => $this->subscriptionUpdateHandlerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $configValueData
+ * @return void
+ * @dataProvider afterSavePluginIsNotApplicableDataProvider
+ */
+ public function testAfterSavePluginIsNotApplicable(
+ array $configValueData
+ ) {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn($configValueData['isValueChanged']);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn($configValueData['path']);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn($configValueData['scope']);
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->never())
+ ->method('processUrlUpdate');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function afterSavePluginIsNotApplicableDataProvider()
+ {
+ return [
+ 'Value has not been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => false,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Unsecure URL has been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_UNSECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Secure URL has been changed not in the Default scope' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeInterface::SCOPE_STORES
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSavePluginIsApplicable()
+ {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn(true);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn(Store::XML_PATH_SECURE_BASE_URL);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ $this->configValueMock
+ ->method('getOldValue')
+ ->willReturn('http://store.com');
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->once())
+ ->method('processUrlUpdate')
+ ->with('http://store.com');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
new file mode 100644
index 0000000000000..0607a977e5b68
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
@@ -0,0 +1,149 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportUrlProvider = $this->objectManagerHelper->getObject(
+ ReportUrlProvider::class,
+ [
+ 'config' => $this->configMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'otpRequest' => $this->otpRequestMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'urlReportConfigPath' => $this->urlReportConfigPath,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isTokenExist
+ * @param string|null $otp If null OTP was not received.
+ *
+ * @dataProvider getUrlDataProvider
+ */
+ public function testGetUrl($isTokenExist, $otp)
+ {
+ $reportUrl = 'https://example.com/report';
+ $url = '';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->urlReportConfigPath)
+ ->willReturn($reportUrl);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn($isTokenExist);
+ $this->otpRequestMock
+ ->expects($isTokenExist ? $this->once() : $this->never())
+ ->method('call')
+ ->with()
+ ->willReturn($otp);
+ if ($isTokenExist && $otp) {
+ $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&');
+ }
+ $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl());
+ }
+
+ /**
+ * @return array
+ */
+ public function getUrlDataProvider()
+ {
+ return [
+ 'TokenDoesNotExist' => [false, null],
+ 'TokenExistAndOtpEmpty' => [true, null],
+ 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetUrlWhenSubscriptionUpdateRunning()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn('http://store.com');
+ $this->expectException(SubscriptionUpdateException::class);
+ $this->reportUrlProvider->getUrl();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
new file mode 100644
index 0000000000000..d9b030b84d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
@@ -0,0 +1,213 @@
+configInterfaceMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass();
+ $this->reportValidatorMock = $this->getMockBuilder(ReportValidator::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->providerFactoryMock = $this->getMockBuilder(ProviderFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->reportProviderMock = $this->getMockBuilder(ReportProvider::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportWriter = $this->objectManagerHelper->getObject(
+ ReportWriter::class,
+ [
+ 'config' => $this->configInterfaceMock,
+ 'reportValidator' => $this->reportValidatorMock,
+ 'providerFactory' => $this->providerFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWrite(array $configData)
+ {
+ $errors = [];
+ $fileData = [
+ ['number' => 1, 'type' => 'Shoes Usual']
+ ];
+ $this->configInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with()
+ ->willReturn([$configData]);
+ $this->providerFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->providerClass)
+ ->willReturn($this->reportProviderMock);
+ $parameterName = isset(reset($configData)[0]['parameters']['name'])
+ ? reset($configData)[0]['parameters']['name']
+ : '';
+ $this->reportProviderMock->expects($this->once())
+ ->method('getReport')
+ ->with($parameterName ?: null)
+ ->willReturn($fileData);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock
+ ->expects($this->once())
+ ->method('lock')
+ ->with();
+ $errorStreamMock
+ ->expects($this->exactly(2))
+ ->method('writeCsv')
+ ->withConsecutive(
+ [array_keys($fileData[0])],
+ [$fileData[0]]
+ );
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ if ($parameterName) {
+ $this->reportValidatorMock
+ ->expects($this->once())
+ ->method('validate')
+ ->with($parameterName)
+ ->willReturn($errors);
+ }
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('openFile')
+ ->with(
+ $this->stringContains('/var/tmp' . $parameterName ?: $this->reportName),
+ 'w+'
+ )->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWriteErrorFile($configData)
+ {
+ $errors = ['orders', 'SQL Error: test'];
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock->expects($this->once())->method('lock');
+ $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors);
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ $this->reportValidatorMock->expects($this->once())->method('validate')->willReturn($errors);
+ $this->directoryMock->expects($this->once())->method('openFile')->with('/var/tmp' . 'errors.csv', 'w+')
+ ->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testWriteEmptyReports()
+ {
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]);
+ $this->reportValidatorMock->expects($this->never())->method('validate');
+ $this->directoryMock->expects($this->never())->method('openFile');
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return array
+ */
+ public function configDataProvider()
+ {
+ return [
+ 'reportProvider' => [
+ [
+ 'providers' => [
+ [
+ 'name' => $this->providerName,
+ 'class' => $this->providerClass,
+ 'parameters' => [
+ 'name' => $this->reportName
+ ],
+ ]
+ ]
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
new file mode 100644
index 0000000000000..f314d77f32b41
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
@@ -0,0 +1,50 @@
+moduleManagerMock = $this->getMockBuilder(ModuleManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->moduleIterator = $objectManagerHelper->getObject(
+ ModuleIterator::class,
+ [
+ 'moduleManager' => $this->moduleManagerMock,
+ 'iterator' => new \ArrayIterator([0 => ['module_name' => 'Coco_Module']])
+ ]
+ );
+ }
+
+ public function testCurrent()
+ {
+ $this->moduleManagerMock->expects($this->once())
+ ->method('isEnabled')
+ ->with('Coco_Module')
+ ->willReturn(true);
+ foreach ($this->moduleIterator as $item) {
+ $this->assertEquals(['module_name' => 'Coco_Module', 'status' => 'Enabled'], $item);
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
new file mode 100644
index 0000000000000..cc46d175543ad
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
@@ -0,0 +1,123 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeMock = $this->getMockBuilder(StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configPaths = [
+ 'web/unsecure/base_url',
+ 'currency/options/base',
+ 'general/locale/timezone'
+ ];
+
+ $this->storeConfigurationProvider = new StoreConfigurationProvider(
+ $this->scopeConfigMock,
+ $this->storeManagerMock,
+ $this->configPaths
+ );
+ }
+
+ public function testGetReport()
+ {
+ $map = [
+ ['web/unsecure/base_url', 'default', 0, '127.0.0.1'],
+ ['currency/options/base', 'default', 0, 'USD'],
+ ['general/locale/timezone', 'default', 0, 'America/Dawson'],
+ ['web/unsecure/base_url', 'websites', 1, '127.0.0.2'],
+ ['currency/options/base', 'websites', 1, 'USD'],
+ ['general/locale/timezone', 'websites', 1, 'America/Belem'],
+ ['web/unsecure/base_url', 'stores', 2, '127.0.0.3'],
+ ['currency/options/base', 'stores', 2, 'USD'],
+ ['general/locale/timezone', 'stores', 2, 'America/Phoenix'],
+ ];
+
+ $this->scopeConfigMock
+ ->method('getValue')
+ ->will($this->returnValueMap($map));
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getWebsites')
+ ->willReturn([$this->websiteMock]);
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getStores')
+ ->willReturn([$this->storeMock]);
+
+ $this->websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->storeMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(2);
+ $result = iterator_to_array($this->storeConfigurationProvider->getReport());
+ $resultValues = [];
+ foreach ($result as $item) {
+ $resultValues[] = array_values($item);
+ }
+ array_multisort($resultValues);
+ array_multisort($map);
+ $this->assertEquals($resultValues, $map);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
new file mode 100644
index 0000000000000..d6b041ce03178
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
@@ -0,0 +1,196 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->statusProvider = $this->objectManagerHelper->getObject(
+ SubscriptionStatusProvider::class,
+ [
+ 'scopeConfig' => $this->scopeConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @dataProvider getStatusShouldBeFailedDataProvider
+ */
+ public function testGetStatusShouldBeFailed(array $flagManagerData)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::FAILED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBeFailedDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ 'Subscription update is active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @param bool $isTokenExist
+ * @dataProvider getStatusShouldBePendingDataProvider
+ */
+ public function testGetStatusShouldBePending(array $flagManagerData, bool $isTokenExist)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn($isTokenExist);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::PENDING, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBePendingDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and token exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ 'isTokenExist' => true,
+ ],
+ ];
+ }
+
+ public function testGetStatusShouldBeEnabled()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(null);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+ $this->assertEquals(SubscriptionStatusProvider::ENABLED, $this->statusProvider->getStatus());
+ }
+
+ public function testGetStatusShouldBeDisabled()
+ {
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(false);
+ $this->assertEquals(SubscriptionStatusProvider::DISABLED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @param array $mapping
+ */
+ private function expectFlagManagerReturn(array $mapping)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->willReturnMap($mapping);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
new file mode 100644
index 0000000000000..ad1d87488d751
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
@@ -0,0 +1,106 @@
+subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->urlBuilderMock = $this->getMockBuilder(UrlInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->notification = $this->objectManagerHelper->getObject(
+ NotificationAboutFailedSubscription::class,
+ [
+ 'subscriptionStatusProvider' => $this->subscriptionStatusMock,
+ 'urlBuilder' => $this->urlBuilderMock
+ ]
+ );
+ }
+
+ public function testIsDisplayedWhenMessageShouldBeDisplayed()
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn(
+ SubscriptionStatusProvider::FAILED
+ );
+ $this->assertTrue($this->notification->isDisplayed());
+ }
+
+ /**
+ * @dataProvider notDisplayedNotificationStatuses
+ *
+ * @param $status
+ */
+ public function testIsDisplayedWhenMessageShouldNotBeDisplayed($status)
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn($status);
+ $this->assertFalse($this->notification->isDisplayed());
+ }
+
+ public function testGetTextShouldBuildMessage()
+ {
+ $retryUrl = 'http://magento.dev/retryUrl';
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('analytics/subscription/retry')
+ ->willReturn($retryUrl);
+ $messageDetails = 'Failed to synchronize data to the Magento Business Intelligence service. ';
+ $messageDetails .= sprintf('Retry Synchronization ', $retryUrl);
+ $this->assertEquals($messageDetails, $this->notification->getText());
+ }
+
+ /**
+ * Provide statuses according to which message should not be displayed.
+ *
+ * @return array
+ */
+ public function notDisplayedNotificationStatuses()
+ {
+ return [
+ [SubscriptionStatusProvider::PENDING],
+ [SubscriptionStatusProvider::DISABLED],
+ [SubscriptionStatusProvider::ENABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
new file mode 100644
index 0000000000000..3f1ed9a5cf4c0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
@@ -0,0 +1,121 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\Config\Converter\Xml::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvertNoElements()
+ {
+ $this->assertEmpty(
+ $this->subject->convert(new \DOMDocument())
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvert()
+ {
+ $dom = new \DOMDocument();
+
+ $expectedArray = [
+ 'config' => [
+ [
+ 'noNamespaceSchemaLocation' => 'urn:magento:module:Magento_Analytics:etc/reports.xsd',
+ 'report' => [
+ [
+ 'name' => 'test_report_1',
+ 'connection' => 'sales',
+ 'source' => [
+ [
+ 'name' => 'sales_order',
+ 'alias' => 'orders',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id',
+ 'alias' => 'identifier',
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'gt',
+ '_value' => '10'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'name' => 'test_report_2',
+ 'connection' => 'default',
+ 'source' => [
+ [
+ 'name' => 'customer_entity',
+ 'alias' => 'customers',
+ 'attribute' => [
+ [
+ 'name' => 'email'
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'dob',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $dom->loadXML(file_get_contents(__DIR__ . '/../_files/valid_reports.xml'));
+
+ $this->assertEquals($expectedArray, $this->subject->convert($dom));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
new file mode 100644
index 0000000000000..85343b6b301d6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
@@ -0,0 +1,47 @@
+mapper = new Mapper();
+ }
+
+ public function testExecute()
+ {
+ $configData['config'][0]['report'] = [
+ [
+ 'source' => ['product'],
+ 'name' => 'Product',
+ ]
+ ];
+ $expectedResult = [
+ 'Product' => [
+ 'source' => 'product',
+ 'name' => 'Product',
+ ]
+ ];
+ $this->assertEquals($this->mapper->execute($configData), $expectedResult);
+ }
+
+ public function testExecuteWithoutReports()
+ {
+ $configData = [];
+ $this->assertEquals($this->mapper->execute($configData), []);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
new file mode 100644
index 0000000000000..e04ee96163797
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
new file mode 100644
index 0000000000000..cbc9aa129d874
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
@@ -0,0 +1,64 @@
+dataMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataMock,
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $queryName = 'query string';
+ $queryResult = [ 'query' => 1 ];
+
+ $this->dataMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryResult);
+
+ $this->assertSame($queryResult, $this->config->get($queryName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
new file mode 100644
index 0000000000000..1e4ae9142c13d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
@@ -0,0 +1,106 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionNewMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->connectionFactory = $this->objectManagerHelper->getObject(
+ ConnectionFactory::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testGetConnection()
+ {
+ $connectionName = 'read';
+
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('getConfig')
+ ->with()
+ ->willReturn(['persistent' => 1]);
+
+ $this->objectManagerMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]])
+ ->willReturn($this->connectionNewMock);
+
+ $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
new file mode 100644
index 0000000000000..3b01105a8873b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
@@ -0,0 +1,143 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleNotEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+
+ $this->conditionResolverMock->expects($this->once())
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['filter'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(sales.entity_id IS NULL)');
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with(['(sales.entity_id IS NULL)']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
new file mode 100644
index 0000000000000..575db94a7b7e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
@@ -0,0 +1,167 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler::class,
+ [
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider assembleDataProvider
+ * @param array $queryConfig
+ * @param string $tableName
+ * @return void
+ */
+ public function testAssemble(array $queryConfig, $tableName)
+ {
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['alias']);
+
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['name']);
+
+ $this->resourceConnection
+ ->expects($this->once())
+ ->method('getTableName')
+ ->with($queryConfig['source']['name'])
+ ->willReturn($tableName);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->with([$queryConfig['source']['alias'] => $tableName]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfig['source'])
+ ->willReturn(['entity_id' => 'sales.entity_id']);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(['entity_id' => 'sales.entity_id']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfig)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleDataProvider()
+ {
+ return [
+ 'Tables without prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'sales_order',
+ ],
+ 'Tables with prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'pref_sales_order',
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
new file mode 100644
index 0000000000000..aaafd731552a0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
@@ -0,0 +1,279 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getJoins')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setColumns');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setJoins');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @param array $queryConfigMock
+ * @param array $joinsMock
+ * @param array $tablesMapping
+ * @return void
+ * @dataProvider assembleNotEmptyDataProvider
+ */
+ public function testAssembleNotEmpty(array $queryConfigMock, array $joinsMock, array $tablesMapping)
+ {
+ $filtersMock = [];
+
+ $this->nameResolverMock->expects($this->at(0))
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+ $this->nameResolverMock->expects($this->at(1))
+ ->method('getAlias')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['alias']);
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['name']);
+
+ $this->resourceConnection
+ ->expects($this->any())
+ ->method('getTableName')
+ ->willReturnOnConsecutiveCalls(...array_values($tablesMapping));
+
+ $this->conditionResolverMock->expects($this->at(0))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['using'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(billing.parent_id = `sales`.`entity_id`)');
+
+ if (isset($queryConfigMock['source']['link-source'][0]['filter'])) {
+ $filtersMock = ['(sales.entity_id IS NULL)'];
+
+ $this->conditionResolverMock->expects($this->at(1))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['filter'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn($filtersMock[0]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfigMock['source']['link-source'][0])
+ ->willReturn(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+ }
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with($filtersMock);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setJoins')
+ ->with($joinsMock);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleNotEmptyDataProvider()
+ {
+ return [
+ [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'link-source' => [
+ [
+ 'name' => 'sales_order_address',
+ 'alias' => 'billing',
+ 'link-type' => 'left',
+ 'attribute' => [
+ [
+ 'alias' => 'billing_address_id',
+ 'name' => 'entity_id'
+ ]
+ ],
+ 'using' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'parent_id',
+ 'operator' => 'eq',
+ 'type' => 'identifier',
+ '_value' => 'entity_id'
+ ]
+ ]
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'billing' => [
+ 'link-type' => 'left',
+ 'table' => [
+ 'billing' => 'pref_sales_order_address'
+ ],
+ 'condition' => '(billing.parent_id = `sales`.`entity_id`)'
+ ]
+ ],
+ ['sales_order_address' => 'pref_sales_order_address']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
new file mode 100644
index 0000000000000..bdbe3d1d22c22
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
@@ -0,0 +1,150 @@
+selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManagerHelper($this);
+ $this->columnsResolver = $objectManager->getObject(
+ ColumnsResolver::class,
+ [
+ 'nameResolver' => new NameResolver(),
+ 'resourceConnection' => $this->resourceConnectionMock
+ ]
+ );
+ }
+
+ public function testGetColumnsWithoutAttributes()
+ {
+ $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []);
+ }
+
+ /**
+ * @dataProvider getColumnsDataProvider
+ */
+ public function testGetColumnsWithFunction($expectedColumns, $expectedGroup, $entityConfig)
+ {
+ $this->resourceConnectionMock->expects($this->any())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->any())
+ ->method('quoteIdentifier')
+ ->with('cpe.name')
+ ->willReturn('`cpe`.`name`');
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getGroup')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setGroup')
+ ->with($expectedGroup);
+ $this->assertEquals(
+ $expectedColumns,
+ $this->columnsResolver->getColumns(
+ $this->selectBuilderMock,
+ $entityConfig
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function getColumnsDataProvider()
+ {
+ return [
+ 'COUNT( DISTINCT `cpe`.`name`) AS name' => [
+ 'expectedColumns' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'expectedGroup' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'function' => 'COUNT',
+ 'distinct' => true,
+ 'group' => true
+ ]
+ ],
+ ],
+ ],
+ 'AVG(`cpe`.`name`) AS avg_name' => [
+ 'expectedColumns' => [
+ 'avg_name' => new ColumnValueExpression('AVG(`cpe`.`name`)')
+ ],
+ 'expectedGroup' => [],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'alias' => 'avg_name',
+ 'function' => 'AVG',
+ ]
+ ],
+ ],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
new file mode 100644
index 0000000000000..c8182d068fba5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
@@ -0,0 +1,108 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolver = new ConditionResolver($this->resourceConnectionMock);
+ }
+
+ public function testGetFilter()
+ {
+ $condition = ["type" => "variable", "_value" => "1", "attribute" => "id", "operator" => "neq"];
+ $valueCondition = ["type" => "value", "_value" => "2", "attribute" => "first_name", "operator" => "eq"];
+ $identifierCondition = [
+ "type" => "identifier",
+ "_value" => "other_field",
+ "attribute" => "last_name",
+ "operator" => "eq"];
+ $filter = [["glue" => "AND", "condition" => [$valueCondition]]];
+ $filterConfig = [
+ ["glue" => "OR", "condition" => [$condition], 'filter' => $filter],
+ ["glue" => "OR", "condition" => [$identifierCondition]],
+ ];
+ $aliasName = 'n';
+ $this->selectBuilderMock->expects($this->any())
+ ->method('setParams')
+ ->with(array_merge([], [$condition['_value']]));
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getParams')
+ ->willReturn([]);
+
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn(['price' => new Expression("(n.price = 400)")]);
+
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock->expects($this->any())
+ ->method('quote')
+ ->willReturn("'John'");
+ $this->connectionMock->expects($this->exactly(4))
+ ->method('quoteIdentifier')
+ ->willReturnMap([
+ ['n.id', false, '`n`.`id`'],
+ ['n.first_name', false, '`n`.`first_name`'],
+ ['n.last_name', false, '`n`.`last_name`'],
+ ['other_field', false, '`other_field`'],
+ ]);
+
+ $result = "(`n`.`id` != 1 OR ((`n`.`first_name` = 'John'))) OR (`n`.`last_name` = `other_field`)";
+ $this->assertEquals(
+ $result,
+ $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
new file mode 100644
index 0000000000000..4accd03aef3ea
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
@@ -0,0 +1,90 @@
+nameResolverMock = $this->getMockBuilder(NameResolver::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName'])
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->nameResolver = $this->objectManagerHelper->getObject(NameResolver::class);
+ }
+
+ public function testGetName()
+ {
+ $elementConfigMock = [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ ];
+
+ $this->assertSame('sales_order', $this->nameResolver->getName($elementConfigMock));
+ }
+
+ /**
+ * @param array $elementConfig
+ * @param string|null $elementAlias
+ *
+ * @dataProvider getAliasDataProvider
+ */
+ public function testGetAlias($elementConfig, $elementAlias)
+ {
+ $elementName = 'elementName';
+
+ $this->nameResolverMock
+ ->expects($this->once())
+ ->method('getName')
+ ->with($elementConfig)
+ ->willReturn($elementName);
+
+ $this->assertSame($elementAlias ?: $elementName, $this->nameResolverMock->getAlias($elementConfig));
+ }
+
+ /**
+ * @return array
+ */
+ public function getAliasDataProvider()
+ {
+ return [
+ 'ElementConfigWithAliases' => [
+ ['alias' => 'sales', 'name' => 'sales_order'],
+ 'sales',
+ ],
+ 'ElementConfigWithoutAliases' => [
+ ['name' => 'sales_order'],
+ null,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
new file mode 100644
index 0000000000000..bbb9ca4b511b6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
@@ -0,0 +1,125 @@
+connectionFactoryMock = $this->getMockBuilder(ConnectionFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryFactoryMock = $this->getMockBuilder(QueryFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryMock = $this->getMockBuilder(Query::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass();
+ $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportValidator = $this->objectManagerHelper->getObject(
+ ReportValidator::class,
+ [
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'queryFactory' => $this->queryFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider errorDataProvider
+ * @param string $reportName
+ * @param array $result
+ * @param \PHPUnit_Framework_MockObject_Stub $queryReturnStub
+ */
+ public function testValidate($reportName, $result, \PHPUnit_Framework_MockObject_Stub $queryReturnStub)
+ {
+ $connectionName = 'testConnection';
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->queryMock);
+ $this->queryMock->expects($this->once())->method('getConnectionName')->willReturn($connectionName);
+ $this->connectionFactoryMock->expects($this->once())->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->queryMock->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())->method('limit')->with(0);
+ $this->connectionMock->expects($this->once())->method('query')->with($this->selectMock)->will($queryReturnStub);
+ $this->assertEquals($result, $this->reportValidator->validate($reportName));
+ }
+
+ /**
+ * Provide variations of the error returning
+ *
+ * @return array
+ */
+ public function errorDataProvider()
+ {
+ $reportName = 'test';
+ $errorMessage = 'SQL Error 42';
+ return [
+ [
+ $reportName,
+ 'expectedResult' => [],
+ 'queryReturnStub' => $this->returnValue(null)
+ ],
+ [
+ $reportName,
+ 'expectedResult' => [$reportName, $errorMessage],
+ 'queryReturnStub' => $this->throwException(new \Zend_Db_Statement_Exception($errorMessage))
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
new file mode 100644
index 0000000000000..a82a187cdb3f8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
@@ -0,0 +1,103 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilder = new SelectBuilder($this->resourceConnectionMock);
+ }
+
+ public function testCreate()
+ {
+ $connectionName = 'MySql';
+ $from = ['customer c'];
+ $columns = ['id', 'name', 'price'];
+ $filter = 'filter';
+ $joins = [
+ ['link-type' => 'left', 'table' => 'customer', 'condition' => 'in'],
+ ['link-type' => 'inner', 'table' => 'price', 'condition' => 'eq'],
+ ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'],
+ ];
+ $groups = ['id', 'name'];
+ $this->selectBuilder->setConnectionName($connectionName);
+ $this->selectBuilder->setFrom($from);
+ $this->selectBuilder->setColumns($columns);
+ $this->selectBuilder->setFilters([$filter]);
+ $this->selectBuilder->setJoins($joins);
+ $this->selectBuilder->setGroup($groups);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())
+ ->method('from')
+ ->with($from, []);
+ $this->selectMock->expects($this->once())
+ ->method('columns')
+ ->with($columns);
+ $this->selectMock->expects($this->once())
+ ->method('where')
+ ->with($filter);
+ $this->selectMock->expects($this->once())
+ ->method('joinLeft')
+ ->with($joins[0]['table'], $joins[0]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinInner')
+ ->with($joins[1]['table'], $joins[1]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinRight')
+ ->with($joins[2]['table'], $joins[2]['condition'], []);
+ $this->selectBuilder->create();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
new file mode 100644
index 0000000000000..1d3f293ed676a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
@@ -0,0 +1,59 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorIteratorMock = $this->getMockBuilder(\IteratorIterator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactory = new IteratorFactory(
+ $this->objectManagerMock
+ );
+ }
+
+ public function testCreate()
+ {
+ $arrayObject = new \ArrayIterator([1, 2, 3, 4, 5]);
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(\IteratorIterator::class, ['iterator' => $arrayObject])
+ ->willReturn($this->iteratorIteratorMock);
+
+ $this->assertEquals($this->iteratorFactory->create($arrayObject), $this->iteratorIteratorMock);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
new file mode 100644
index 0000000000000..9a3805a50f167
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
@@ -0,0 +1,239 @@
+queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Config::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->assemblerMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryCacheMock = $this->getMockBuilder(
+ \Magento\Framework\App\CacheInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(
+ \Magento\Framework\ObjectManagerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\SelectHydrator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilderFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\QueryFactory::class,
+ [
+ 'config' => $this->configMock,
+ 'selectBuilderFactory' => $this->selectBuilderFactoryMock,
+ 'assemblers' => [$this->assemblerMock],
+ 'queryCache' => $this->queryCacheMock,
+ 'objectManager' => $this->objectManagerMock,
+ 'selectHydrator' => $this->selectHydratorMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateCached()
+ {
+ $queryName = 'test_query';
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn('{"connectionName":"sales","config":{},"select_parts":{}}');
+
+ $this->selectHydratorMock->expects($this->any())
+ ->method('recreate')
+ ->with([])
+ ->willReturn($this->selectMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => 'sales',
+ 'config' => []
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->never())
+ ->method('save');
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateNotCached()
+ {
+ $queryName = 'test_query';
+
+ $queryConfigMock = [
+ 'name' => 'test_query',
+ 'connection' => 'sales'
+ ];
+
+ $selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $selectBuilderMock->expects($this->once())
+ ->method('setConnectionName')
+ ->with($queryConfigMock['connection']);
+ $selectBuilderMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->selectMock);
+ $selectBuilderMock->expects($this->any())
+ ->method('getConnectionName')
+ ->willReturn($queryConfigMock['connection']);
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn(null);
+
+ $this->configMock->expects($this->any())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryConfigMock);
+
+ $this->selectBuilderFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($selectBuilderMock);
+
+ $this->assemblerMock->expects($this->once())
+ ->method('assemble')
+ ->with($selectBuilderMock, $queryConfigMock)
+ ->willReturn($selectBuilderMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => $queryConfigMock['connection'],
+ 'config' => $queryConfigMock
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->once())
+ ->method('save')
+ ->with(json_encode($this->queryMock), $queryName);
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
new file mode 100644
index 0000000000000..a4b08a9ce5e0a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
@@ -0,0 +1,90 @@
+selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(selectHydrator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->query = $this->objectManagerHelper->getObject(
+ Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'connectionName' => $this->connectionName,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'config' => []
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testJsonSerialize()
+ {
+ $selectParts = ['part' => 1];
+
+ $this->selectHydratorMock
+ ->expects($this->once())
+ ->method('extract')
+ ->with($this->selectMock)
+ ->willReturn($selectParts);
+
+ $expectedResult = [
+ 'connectionName' => $this->connectionName,
+ 'select_parts' => $selectParts,
+ 'config' => []
+ ];
+
+ $this->assertSame($expectedResult, $this->query->jsonSerialize());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
new file mode 100644
index 0000000000000..5f329993dd291
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
@@ -0,0 +1,180 @@
+selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->queryMock->expects($this->any())
+ ->method('getSelect')
+ ->willReturn($this->selectMock);
+
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->statementMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Statement\Pdo\Mysql::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->statementMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn($this->iteratorMock);
+
+ $this->connectionMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Adapter\AdapterInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\QueryFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\IteratorFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->connectionFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\ConnectionFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\ReportProvider::class,
+ [
+ 'queryFactory' => $this->queryFactoryMock,
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'iteratorFactory' => $this->iteratorFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetReport()
+ {
+ $reportName = 'test_report';
+ $connectionName = 'sales';
+
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($reportName)
+ ->willReturn($this->queryMock);
+
+ $this->connectionFactoryMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConnectionName')
+ ->willReturn($connectionName);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConfig')
+ ->willReturn(
+ [
+ 'connection' => $connectionName
+ ]
+ );
+
+ $this->connectionMock->expects($this->once())
+ ->method('query')
+ ->with($this->selectMock)
+ ->willReturn($this->statementMock);
+
+ $this->iteratorFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($this->statementMock, null)
+ ->willReturn($this->iteratorMock);
+ $this->assertEquals($this->iteratorMock, $this->subject->getReport($reportName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
new file mode 100644
index 0000000000000..ce57a1eca3689
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
@@ -0,0 +1,257 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->selectHydrator = $this->objectManagerHelper->getObject(
+ SelectHydrator::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testExtract()
+ {
+ $selectParts =
+ [
+ Select::DISTINCT,
+ Select::COLUMNS,
+ Select::UNION,
+ Select::FROM,
+ Select::WHERE,
+ Select::GROUP,
+ Select::HAVING,
+ Select::ORDER,
+ Select::LIMIT_COUNT,
+ Select::LIMIT_OFFSET,
+ Select::FOR_UPDATE
+ ];
+
+ $result = [];
+ foreach ($selectParts as $part) {
+ $result[$part] = "Part";
+ }
+ $this->selectMock->expects($this->any())
+ ->method('getPart')
+ ->willReturn("Part");
+ $this->assertEquals($this->selectHydrator->extract($this->selectMock), $result);
+ }
+
+ /**
+ * @dataProvider recreateWithoutExpressionDataProvider
+ * @param array $selectParts
+ * @param array $parts
+ * @param array $partValues
+ */
+ public function testRecreateWithoutExpression($selectParts, $parts, $partValues)
+ {
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ foreach ($parts as $key => $part) {
+ $this->selectMock->expects($this->at($key))
+ ->method('setPart')
+ ->with($part, $partValues[$key]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithoutExpressionDataProvider()
+ {
+ return [
+ 'Select without expressions' => [
+ [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]
+ ],
+ [Select::COLUMNS],
+ [[
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider recreateWithExpressionDataProvider
+ * @param array $selectParts
+ * @param array $expectedParts
+ * @param \PHPUnit_Framework_MockObject_MockObject[] $expressionMocks
+ */
+ public function testRecreateWithExpression(
+ array $selectParts,
+ array $expectedParts,
+ array $expressionMocks
+ ) {
+ $this->objectManagerMock
+ ->expects($this->exactly(count($expressionMocks)))
+ ->method('create')
+ ->with($this->isType('string'), $this->isType('array'))
+ ->willReturnOnConsecutiveCalls(...$expressionMocks);
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with()
+ ->willReturn($this->connectionMock);
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('select')
+ ->with()
+ ->willReturn($this->selectMock);
+ foreach (array_keys($selectParts) as $key => $partName) {
+ $this->selectMock
+ ->expects($this->at($key))
+ ->method('setPart')
+ ->with($partName, $expectedParts[$partName]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithExpressionDataProvider()
+ {
+ $expressionMock = $this->getMockBuilder(JsonSerializableExpression::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return [
+ 'Select without expressions' => [
+ 'Parts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ [
+ 'class' => 'Some_class',
+ 'arguments' => [
+ 'expression' => ['some(expression)']
+ ]
+ ],
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedParts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ $expressionMock,
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedExpressions' => [
+ $expressionMock
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json
new file mode 100644
index 0000000000000..b17bb10cb4112
--- /dev/null
+++ b/app/code/Magento/Analytics/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "magento/module-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/module-backend": "100.2.*",
+ "magento/module-config": "100.2.*",
+ "magento/module-integration": "100.2.*",
+ "magento/module-store": "100.2.*",
+ "magento/framework": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Analytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/docs/images/M2_MA_signup.png b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png
new file mode 100644
index 0000000000000..78ed8fad92881
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/analytics_modules.png b/app/code/Magento/Analytics/docs/images/analytics_modules.png
new file mode 100644
index 0000000000000..0bf6048b0d9cc
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/analytics_modules.png differ
diff --git a/app/code/Magento/Analytics/docs/images/data_transition.png b/app/code/Magento/Analytics/docs/images/data_transition.png
new file mode 100644
index 0000000000000..a75e97983e15d
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/data_transition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/definition.png b/app/code/Magento/Analytics/docs/images/definition.png
new file mode 100644
index 0000000000000..16acc576320b0
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/definition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png
new file mode 100644
index 0000000000000..f39d2e4900703
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png differ
diff --git a/app/code/Magento/Analytics/docs/images/signup.png b/app/code/Magento/Analytics/docs/images/signup.png
new file mode 100644
index 0000000000000..561e18b3a351f
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update.png b/app/code/Magento/Analytics/docs/images/update.png
new file mode 100644
index 0000000000000..149f5b5d3f9bd
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update_request.png b/app/code/Magento/Analytics/docs/images/update_request.png
new file mode 100644
index 0000000000000..7181251e3634e
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update_request.png differ
diff --git a/app/code/Magento/Analytics/etc/acl.xml b/app/code/Magento/Analytics/etc/acl.xml
new file mode 100644
index 0000000000000..bf2251895f929
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/acl.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/di.xml b/app/code/Magento/Analytics/etc/adminhtml/di.xml
new file mode 100644
index 0000000000000..5e305e70e5ad3
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/menu.xml b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
new file mode 100644
index 0000000000000..915211c4bb85e
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/routes.xml b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..0ae2762dacc5f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..889517e629e04
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Advanced Reporting
+ general
+ Magento_Analytics::analytics_settings
+
+ Advanced Reporting
+ For more information, see our
+ terms and conditions .]]>
+
+ Advanced Reporting Service
+ Magento\Config\Model\Config\Source\Enabledisable
+ Magento\Analytics\Model\Config\Backend\Enabled
+ Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel
+ analytics/subscription/enabled
+
+
+ Time of day to send data
+ Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel
+ Magento\Analytics\Model\Config\Backend\CollectionTime
+
+
+ Industry Data
+ Industry
+ In order to personalize your Advanced Reporting experience, please select your industry.
+ Magento\Analytics\Model\Config\Source\Vertical
+ Magento\Analytics\Model\Config\Backend\Vertical
+ Magento\Analytics\Block\Adminhtml\System\Config\Vertical
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml
new file mode 100644
index 0000000000000..77ebe751a31cf
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stores
+
+
+
+
+
+
+
+
+ websites
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xsd b/app/code/Magento/Analytics/etc/analytics.xsd
new file mode 100644
index 0000000000000..2506e3d6a6a9a
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xsd
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ File name attribute can has only [a-zA-Z0-9/_].
+
+
+
+
+
+
+
+
+
+ Value is required.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml
new file mode 100644
index 0000000000000..b6194ba12993f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/config.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ https://advancedreporting.rjmetrics.com/signup
+ https://advancedreporting.rjmetrics.com/update
+ https://dashboard.rjmetrics.com/v2/magento/signup
+ https://advancedreporting.rjmetrics.com/otp
+ https://advancedreporting.rjmetrics.com/report
+ https://advancedreporting.rjmetrics.com/report
+
+ Magento Analytics user
+
+ 02,00,00
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/crontab.xml b/app/code/Magento/Analytics/etc/crontab.xml
new file mode 100644
index 0000000000000..a4beef0359540
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/crontab.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml
new file mode 100644
index 0000000000000..56657b58475d3
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/di.xml
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\SignUpCommand
+ - Magento\Analytics\Model\Connector\UpdateCommand
+ - Magento\Analytics\Model\Connector\NotifyDataChangedCommand
+
+
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Data
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Reader
+ Magento_Analytics_ReportXml_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/reports.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\ReportXml\Config\SchemaLocator
+ reports.xml
+
+ - name
+ -
+
- name
+ - alias
+
+ - name
+ - name
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\Config\Reader\Xml
+
+
+
+
+
+
+ Magento\Analytics\Model\Config\Data
+
+
+
+
+ Magento\Analytics\Model\Config\Reader
+ Magento_Analytics_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/analytics.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\Model\Config\SchemaLocator
+ analytics.xml
+
+ - name
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\DB\Assembler\FromAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler
+
+
+
+
+
+
+ - Magento\Analytics\Model\Config\Reader\Xml
+
+
+
+
+
+
+ - web/unsecure/base_url
+ - currency/options/base
+ - general/locale/timezone
+ - general/country/default
+ - carriers/dhl/title
+ - carriers/dhl/active
+ - carriers/fedex/title
+ - carriers/fedex/active
+ - carriers/flatrate/title
+ - carriers/flatrate/active
+ - carriers/tablerate/title
+ - carriers/tablerate/active
+ - carriers/freeshipping/title
+ - carriers/freeshipping/active
+ - carriers/ups/title
+ - carriers/ups/active
+ - carriers/usps/title
+ - carriers/usps/active
+ - payment/free/title
+ - payment/free/active
+ - payment/checkmo/title
+ - payment/checkmo/active
+ - payment/purchaseorder/title
+ - payment/purchaseorder/active
+ - payment/banktransfer/title
+ - payment/banktransfer/active
+ - payment/cashondelivery/title
+ - payment/cashondelivery/active
+ - payment/authorizenet_directpost/title
+ - payment/authorizenet_directpost/active
+ - payment/paypal_billing_agreement/title
+ - payment/paypal_billing_agreement/active
+ - payment/braintree/title
+ - payment/braintree/active
+ - payment/braintree_paypal/title
+ - payment/braintree_paypal/active
+ - analytics/general/vertical
+
+
+
+
+
+
+ - Apps and Games
+ - Athletic/Sporting Goods
+ - Art and Design
+ - Auto Parts
+ - Baby/Children’s Apparel, Gear and Toys
+ - Beauty and Cosmetics
+ - Books, Music and Magazines
+ - Crafts and Stationery
+ - Consumer Electronics
+ - Deal Site
+ - Fashion Apparel and Accessories
+ - Food, Beverage and Grocery
+ - Home Goods and Furniture
+ - Home Improvement
+ - Jewelry and Watches
+ - Mass Merchant
+ - Office Supplies
+ - Outdoor and Camping Gear
+ - Pet Goods
+ - Pharma and Medical Devices
+ - Technology B2B
+ - Other
+
+
+
+
+
+
+
+
+
+ - \Magento\Analytics\Model\Connector\ResponseHandler\SignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\Update
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\OTP
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+ SignUpResponseResolver
+
+
+
+
+ UpdateResponseResolver
+
+
+
+
+ OtpResponseResolver
+
+
+
+
+ NotifyDataChangedResponseResolver
+
+
+
diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml
new file mode 100644
index 0000000000000..32ee5d23a4d86
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/module.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml
new file mode 100644
index 0000000000000..8a43658670293
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/etc/reports.xsd b/app/code/Magento/Analytics/etc/reports.xsd
new file mode 100644
index 0000000000000..d0ba4068244fe
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xsd
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml
new file mode 100644
index 0000000000000..8252d039f1d03
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/webapi.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/i18n/en_US.csv b/app/code/Magento/Analytics/i18n/en_US.csv
new file mode 100644
index 0000000000000..516c388feb823
--- /dev/null
+++ b/app/code/Magento/Analytics/i18n/en_US.csv
@@ -0,0 +1,84 @@
+"Subscription status","Subscription status"
+"Sorry, there has been an error processing your request. Please try again later.","Sorry, there has been an error processing your request. Please try again later."
+"Sorry, there was an error processing your registration request to Magento Analytics. Please try again later.","Sorry, there was an error processing your registration request to Magento Analytics. Please try again later."
+"Error occurred during postponement notification","Error occurred during postponement notification"
+"Time value has an unsupported format","Time value has an unsupported format"
+"Cron settings can't be saved","Cron settings can't be saved"
+"There was an error save new configuration value.","There was an error save new configuration value."
+"Please select a vertical.","Please select a vertical."
+"--Please Select--","--Please Select--"
+"Command was not found.","Command was not found."
+"Input data must be string or convertible into string.","Input data must be string or convertible into string."
+"Input data must be non-empty string.","Input data must be non-empty string."
+"Not valid cipher method.","Not valid cipher method."
+"Encryption key can't be empty.","Encryption key can't be empty."
+"Source ""%1"" is not exist","Source ""%1"" is not exist"
+"These arguments can't be empty ""%1""","These arguments can't be empty ""%1"""
+"Cannot find predefined integration user!","Cannot find predefined integration user!"
+"File is not ready yet.","File is not ready yet."
+"Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later.","Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later."
+"Failed to synchronize data to the Magento Business Intelligence service. ","Failed to synchronize data to the Magento Business Intelligence service. "
+"Retry Synchronization ","Retry Synchronization "
+TestMessage,TestMessage
+"Error message","Error message"
+"Apps and Games","Apps and Games"
+"Athletic/Sporting Goods","Athletic/Sporting Goods"
+"Art and Design","Art and Design"
+"Advanced Reporting","Advanced Reporting"
+"Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data.","Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data."
+"View details","View details"
+"Go to Advanced Reporting","Go to Advanced Reporting"
+"An error occurred while subscription process.","An error occurred while subscription process."
+Analytics,Analytics
+API,API
+Configuration,Configuration
+"Business Intelligence","Business Intelligence"
+"BI Essentials","BI Essentials"
+"This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions .
+ ","This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions ."
+"Advanced Reporting Service","Advanced Reporting Service"
+Industry,Industry
+"Time of day to send data","Time of day to send data"
+"Get more insights from Magento Business Intelligence ","Get more insights from Magento Business Intelligence "
+"Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier.","Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier."
+"Auto Parts","Auto Parts"
+"Baby/Children’s Apparel, Gear and Toys","Baby/Children’s Apparel, Gear and Toys"
+"Beauty and Cosmetics","Beauty and Cosmetics"
+"Books, Music and Magazines","Books, Music and Magazines"
+"Crafts and Stationery","Crafts and Stationery"
+"Consumer Electronics","Consumer Electronics"
+"Deal Site","Deal Site"
+"Fashion Apparel and Accessories","Fashion Apparel and Accessories"
+"Food, Beverage and Grocery","Food, Beverage and Grocery"
+"Home Goods and Furniture","Home Goods and Furniture"
+"Home Improvement","Home Improvement"
+"Jewelry and Watches","Jewelry and Watches"
+"Mass Merchant","Mass Merchant"
+"Office Supplies","Office Supplies"
+"Outdoor and Camping Gear","Outdoor and Camping Gear"
+"Pet Goods","Pet Goods"
+"Pharma and Medical Devices","Pharma and Medical Devices"
+"Technology B2B","Technology B2B"
+"Analytics Subscription","Analytics Subscription"
+"powered by Magento Business Intelligence","powered by Magento Business Intelligence"
+"Are you sure you want to opt out?","Are you sure you want to opt out?"
+Cancel,Cancel
+"Opt out","Opt out"
+"Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
","Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
"
+"In order to personalize your Advanced Reporting experience, please select your industry.","In order to personalize your Advanced Reporting experience, please select your industry."
diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php
new file mode 100644
index 0000000000000..58d3688b7491d
--- /dev/null
+++ b/app/code/Magento/Analytics/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
new file mode 100644
index 0000000000000..a22c603b2a8b3
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ = $block->escapeHtml(__('Advanced Reporting')) ?>
+
+
+ = $block->escapeHtml(__('Gain new insights and take command of your business\' performance,' .
+ ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?>
+
+
+
+
diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json
index 036f0b4d54a46..3d8cfa49cd1fa 100644
--- a/app/code/Magento/Backend/composer.json
+++ b/app/code/Magento/Backend/composer.json
@@ -24,7 +24,7 @@
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv
index 2730d4d92835b..f9f44f547e25b 100644
--- a/app/code/Magento/Backend/i18n/en_US.csv
+++ b/app/code/Magento/Backend/i18n/en_US.csv
@@ -214,10 +214,13 @@ YTD,YTD
"Admin session lifetime must be greater than or equal to 60 seconds","Admin session lifetime must be greater than or equal to 60 seconds"
Order,Order
"Order #%1","Order #%1"
-"Access denied","Access denied"
-"Please try to sign out and sign in again.","Please try to sign out and sign in again."
-"If you continue to receive this message, please contact the store owner.","If you continue to receive this message, please contact the store owner."
"You need more permissions to access this.","You need more permissions to access this."
+"Sorry, you need permissions to view this content.","Sorry, you need permissions to view this content."
+"Next steps","Next steps"
+"If you think this is an error, try signing out and signing in again.","If you think this is an error, try signing out and signing in again."
+"Contact a system administrator or store owner to gain permissions.","Contact a system administrator or store owner to gain permissions."
+"Return to","Return to"
+"previous page","previous page"
"Welcome, please sign in","Welcome, please sign in"
Username,Username
"user name","user name"
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
index 852ecd5a07962..843328fbf17d7 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
@@ -12,12 +12,23 @@
* @see \Magento\Backend\Block\Denied
*/
?>
-= /* @escapeNotVerified */ __('Access denied') ?>
-hasAvailableResources()): ?>
-
-= /* @escapeNotVerified */ __('Please try to sign out and sign in again.') ?>
-= /* @escapeNotVerified */ __('If you continue to receive this message, please contact the store owner.') ?>
-
-
-= /* @escapeNotVerified */ __('You need more permissions to access this.') ?>
-
+
+
+
= $block->escapeHtml(__('Sorry, you need permissions to view this content.')) ?>
+
= $block->escapeHtml(__('Next steps')) ?>
+
+
diff --git a/app/code/Magento/Braintree/Block/Form.php b/app/code/Magento/Braintree/Block/Form.php
index f6cf62f5a2131..8a727ae87262f 100644
--- a/app/code/Magento/Braintree/Block/Form.php
+++ b/app/code/Magento/Braintree/Block/Form.php
@@ -9,7 +9,6 @@
use Magento\Braintree\Gateway\Config\Config as GatewayConfig;
use Magento\Braintree\Model\Adminhtml\Source\CcType;
use Magento\Braintree\Model\Ui\ConfigProvider;
-use Magento\Framework\App\ObjectManager;
use Magento\Framework\View\Element\Template\Context;
use Magento\Payment\Block\Form\Cc;
use Magento\Payment\Helper\Data;
@@ -21,7 +20,6 @@
*/
class Form extends Cc
{
-
/**
* @var Quote
*/
@@ -48,6 +46,7 @@ class Form extends Cc
* @param Quote $sessionQuote
* @param GatewayConfig $gatewayConfig
* @param CcType $ccType
+ * @param Data $paymentDataHelper
* @param array $data
*/
public function __construct(
@@ -56,12 +55,14 @@ public function __construct(
Quote $sessionQuote,
GatewayConfig $gatewayConfig,
CcType $ccType,
+ Data $paymentDataHelper,
array $data = []
) {
parent::__construct($context, $paymentConfig, $data);
$this->sessionQuote = $sessionQuote;
$this->gatewayConfig = $gatewayConfig;
$this->ccType = $ccType;
+ $this->paymentDataHelper = $paymentDataHelper;
}
/**
@@ -81,7 +82,7 @@ public function getCcAvailableTypes()
*/
public function useCvv()
{
- return $this->gatewayConfig->isCvvEnabled();
+ return $this->gatewayConfig->isCvvEnabled($this->sessionQuote->getStoreId());
}
/**
@@ -90,9 +91,8 @@ public function useCvv()
*/
public function isVaultEnabled()
{
- $storeId = $this->_storeManager->getStore()->getId();
$vaultPayment = $this->getVaultPayment();
- return $vaultPayment->isActive($storeId);
+ return $vaultPayment->isActive($this->sessionQuote->getStoreId());
}
/**
@@ -102,7 +102,10 @@ public function isVaultEnabled()
private function getConfiguredCardTypes()
{
$types = $this->ccType->getCcTypeLabelMap();
- $configCardTypes = array_fill_keys($this->gatewayConfig->getAvailableCardTypes(), '');
+ $configCardTypes = array_fill_keys(
+ $this->gatewayConfig->getAvailableCardTypes($this->sessionQuote->getStoreId()),
+ ''
+ );
return array_intersect_key($types, $configCardTypes);
}
@@ -116,7 +119,11 @@ private function getConfiguredCardTypes()
private function filterCardTypesForCountry(array $configCardTypes, $countryId)
{
$filtered = $configCardTypes;
- $countryCardTypes = $this->gatewayConfig->getCountryAvailableCardTypes($countryId);
+ $countryCardTypes = $this->gatewayConfig->getCountryAvailableCardTypes(
+ $countryId,
+ $this->sessionQuote->getStoreId()
+ );
+
// filter card types only if specific card types are set for country
if (!empty($countryCardTypes)) {
$availableTypes = array_fill_keys($countryCardTypes, '');
@@ -131,19 +138,6 @@ private function filterCardTypesForCountry(array $configCardTypes, $countryId)
*/
private function getVaultPayment()
{
- return $this->getPaymentDataHelper()->getMethodInstance(ConfigProvider::CC_VAULT_CODE);
- }
-
- /**
- * Get payment data helper instance
- * @return Data
- * @deprecated 100.1.0
- */
- private function getPaymentDataHelper()
- {
- if ($this->paymentDataHelper === null) {
- $this->paymentDataHelper = ObjectManager::getInstance()->get(Data::class);
- }
- return $this->paymentDataHelper;
+ return $this->paymentDataHelper->getMethodInstance(ConfigProvider::CC_VAULT_CODE);
}
}
diff --git a/app/code/Magento/Braintree/Block/Payment.php b/app/code/Magento/Braintree/Block/Payment.php
index 8e05856d9b57a..1ba2f862e2fe5 100644
--- a/app/code/Magento/Braintree/Block/Payment.php
+++ b/app/code/Magento/Braintree/Block/Payment.php
@@ -48,6 +48,10 @@ public function getPaymentConfig()
$payment = $this->config->getConfig()['payment'];
$config = $payment[$this->getCode()];
$config['code'] = $this->getCode();
+ $config['clientTokenUrl'] = $this->_urlBuilder->getUrl(
+ 'braintree/payment/getClientToken',
+ ['_secure' => true]
+ );
return json_encode($config, JSON_UNESCAPED_SLASHES);
}
diff --git a/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php b/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php
new file mode 100644
index 0000000000000..af0f1d75665d5
--- /dev/null
+++ b/app/code/Magento/Braintree/Controller/Adminhtml/Payment/GetClientToken.php
@@ -0,0 +1,73 @@
+config = $config;
+ $this->adapterFactory = $adapterFactory;
+ $this->quoteSession = $quoteSession;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute()
+ {
+ $params = [];
+ $response = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+
+ $storeId = $this->quoteSession->getStoreId();
+ $merchantAccountId = $this->config->getMerchantAccountId($storeId);
+ if (!empty($merchantAccountId)) {
+ $params[PaymentDataBuilder::MERCHANT_ACCOUNT_ID] = $merchantAccountId;
+ }
+
+ $clientToken = $this->adapterFactory->create($storeId)
+ ->generate($params);
+ $response->setData(['clientToken' => $clientToken]);
+
+ return $response;
+ }
+}
diff --git a/app/code/Magento/Braintree/Controller/Payment/GetNonce.php b/app/code/Magento/Braintree/Controller/Payment/GetNonce.php
index aecde869ca196..f8b152ded1556 100644
--- a/app/code/Magento/Braintree/Controller/Payment/GetNonce.php
+++ b/app/code/Magento/Braintree/Controller/Payment/GetNonce.php
@@ -62,7 +62,10 @@ public function execute()
try {
$publicHash = $this->getRequest()->getParam('public_hash');
$customerId = $this->session->getCustomerId();
- $result = $this->command->execute(['public_hash' => $publicHash, 'customer_id' => $customerId])->get();
+ $result = $this->command->execute(
+ ['public_hash' => $publicHash, 'customer_id' => $customerId, 'store_id' => $this->session->getStoreId()]
+ )
+ ->get();
$response->setData(['paymentMethodNonce' => $result['paymentMethodNonce']]);
} catch (\Exception $e) {
$this->logger->critical($e);
diff --git a/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php b/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php
index f972eecf9f92d..c8359f69bc47d 100644
--- a/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php
+++ b/app/code/Magento/Braintree/Gateway/Command/CaptureStrategyCommand.php
@@ -6,18 +6,19 @@
namespace Magento\Braintree\Gateway\Command;
use Braintree\Transaction;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Gateway\SubjectReader;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SearchCriteriaBuilder;
-use Magento\Payment\Gateway\Command;
use Magento\Payment\Gateway\Command\CommandPoolInterface;
use Magento\Payment\Gateway\CommandInterface;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Payment\Gateway\Helper\ContextHelper;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
use Magento\Sales\Api\Data\OrderPaymentInterface;
-use Magento\Sales\Api\TransactionRepositoryInterface;
use Magento\Sales\Api\Data\TransactionInterface;
+use Magento\Sales\Api\TransactionRepositoryInterface;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
/**
* Class CaptureStrategyCommand
@@ -66,9 +67,9 @@ class CaptureStrategyCommand implements CommandInterface
private $subjectReader;
/**
- * @var BraintreeAdapter
+ * @var BraintreeAdapterFactory
*/
- private $braintreeAdapter;
+ private $braintreeAdapterFactory;
/**
* @var BraintreeSearchAdapter
@@ -83,7 +84,7 @@ class CaptureStrategyCommand implements CommandInterface
* @param FilterBuilder $filterBuilder
* @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param SubjectReader $subjectReader
- * @param BraintreeAdapter $braintreeAdapter
+ * @param BraintreeAdapterFactory $braintreeAdapterFactory,
* @param BraintreeSearchAdapter $braintreeSearchAdapter
*/
public function __construct(
@@ -92,7 +93,7 @@ public function __construct(
FilterBuilder $filterBuilder,
SearchCriteriaBuilder $searchCriteriaBuilder,
SubjectReader $subjectReader,
- BraintreeAdapter $braintreeAdapter,
+ BraintreeAdapterFactory $braintreeAdapterFactory,
BraintreeSearchAdapter $braintreeSearchAdapter
) {
$this->commandPool = $commandPool;
@@ -100,7 +101,7 @@ public function __construct(
$this->filterBuilder = $filterBuilder;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->subjectReader = $subjectReader;
- $this->braintreeAdapter = $braintreeAdapter;
+ $this->braintreeAdapterFactory = $braintreeAdapterFactory;
$this->braintreeSearchAdapter = $braintreeSearchAdapter;
}
@@ -112,29 +113,29 @@ public function execute(array $commandSubject)
/** @var \Magento\Payment\Gateway\Data\PaymentDataObjectInterface $paymentDO */
$paymentDO = $this->subjectReader->readPayment($commandSubject);
- /** @var \Magento\Sales\Api\Data\OrderPaymentInterface $paymentInfo */
- $paymentInfo = $paymentDO->getPayment();
- ContextHelper::assertOrderPayment($paymentInfo);
-
- $command = $this->getCommand($paymentInfo);
+ $command = $this->getCommand($paymentDO);
$this->commandPool->get($command)->execute($commandSubject);
}
/**
- * Get execution command name
- * @param OrderPaymentInterface $payment
+ * Gets command name.
+ *
+ * @param PaymentDataObjectInterface $paymentDO
* @return string
*/
- private function getCommand(OrderPaymentInterface $payment)
+ private function getCommand(PaymentDataObjectInterface $paymentDO)
{
- // if auth transaction is not exists execute authorize&capture command
+ $payment = $paymentDO->getPayment();
+ ContextHelper::assertOrderPayment($payment);
+
+ // if auth transaction does not exist then execute authorize&capture command
$existsCapture = $this->isExistsCaptureTransaction($payment);
if (!$payment->getAuthorizationTransaction() && !$existsCapture) {
return self::SALE;
}
// do capture for authorization transaction
- if (!$existsCapture && !$this->isExpiredAuthorization($payment)) {
+ if (!$existsCapture && !$this->isExpiredAuthorization($payment, $paymentDO->getOrder())) {
return self::CAPTURE;
}
@@ -143,12 +144,16 @@ private function getCommand(OrderPaymentInterface $payment)
}
/**
+ * Checks if authorization transaction does not expired yet.
+ *
* @param OrderPaymentInterface $payment
- * @return boolean
+ * @param OrderAdapterInterface $orderAdapter
+ * @return bool
*/
- private function isExpiredAuthorization(OrderPaymentInterface $payment)
+ private function isExpiredAuthorization(OrderPaymentInterface $payment, OrderAdapterInterface $orderAdapter)
{
- $collection = $this->braintreeAdapter->search(
+ $adapter = $this->braintreeAdapterFactory->create($orderAdapter->getStoreId());
+ $collection = $adapter->search(
[
$this->braintreeSearchAdapter->id()->is($payment->getLastTransId()),
$this->braintreeSearchAdapter->status()->is(Transaction::AUTHORIZATION_EXPIRED)
diff --git a/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php b/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php
index 91c9f6c14bb5d..64e38d2999676 100644
--- a/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php
+++ b/app/code/Magento/Braintree/Gateway/Command/GetPaymentNonceCommand.php
@@ -7,10 +7,9 @@
namespace Magento\Braintree\Gateway\Command;
use Exception;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
-use Magento\Payment\Gateway\Command;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Payment\Gateway\Command\Result\ArrayResultFactory;
use Magento\Payment\Gateway\CommandInterface;
use Magento\Vault\Api\PaymentTokenManagementInterface;
@@ -27,9 +26,9 @@ class GetPaymentNonceCommand implements CommandInterface
private $tokenManagement;
/**
- * @var BraintreeAdapter
+ * @var BraintreeAdapterFactory
*/
- private $adapter;
+ private $adapterFactory;
/**
* @var ArrayResultFactory
@@ -48,20 +47,20 @@ class GetPaymentNonceCommand implements CommandInterface
/**
* @param PaymentTokenManagementInterface $tokenManagement
- * @param BraintreeAdapter $adapter
+ * @param BraintreeAdapterFactory $adapterFactory
* @param ArrayResultFactory $resultFactory
* @param SubjectReader $subjectReader
* @param PaymentNonceResponseValidator $responseValidator
*/
public function __construct(
PaymentTokenManagementInterface $tokenManagement,
- BraintreeAdapter $adapter,
+ BraintreeAdapterFactory $adapterFactory,
ArrayResultFactory $resultFactory,
SubjectReader $subjectReader,
PaymentNonceResponseValidator $responseValidator
) {
$this->tokenManagement = $tokenManagement;
- $this->adapter = $adapter;
+ $this->adapterFactory = $adapterFactory;
$this->resultFactory = $resultFactory;
$this->subjectReader = $subjectReader;
$this->responseValidator = $responseValidator;
@@ -80,7 +79,9 @@ public function execute(array $commandSubject)
throw new Exception('No available payment tokens');
}
- $data = $this->adapter->createNonce($paymentToken->getGatewayToken());
+ $storeId = $this->subjectReader->readStoreId($commandSubject);
+ $data = $this->adapterFactory->create($storeId)
+ ->createNonce($paymentToken->getGatewayToken());
$result = $this->responseValidator->validate(['response' => ['object' => $data]]);
if (!$result->isValid()) {
diff --git a/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php b/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php
index 9dd52dc91afb9..0466216bdb7d2 100644
--- a/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Config/CanVoidHandler.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Config\ValueHandlerInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Gateway/Config/Config.php b/app/code/Magento/Braintree/Gateway/Config/Config.php
index 7badd2b87ac34..01bb32fbb1a7e 100644
--- a/app/code/Magento/Braintree/Gateway/Config/Config.php
+++ b/app/code/Magento/Braintree/Gateway/Config/Config.php
@@ -68,11 +68,12 @@ public function __construct(
/**
* Return the country specific card type config
*
+ * @param int|null $storeId
* @return array
*/
- public function getCountrySpecificCardTypeConfig()
+ public function getCountrySpecificCardTypeConfig($storeId = null)
{
- $countryCardTypes = $this->getValue(self::KEY_COUNTRY_CREDIT_CARD);
+ $countryCardTypes = $this->getValue(self::KEY_COUNTRY_CREDIT_CARD, $storeId);
if (!$countryCardTypes) {
return [];
}
@@ -83,11 +84,12 @@ public function getCountrySpecificCardTypeConfig()
/**
* Retrieve available credit card types
*
+ * @param int|null $storeId
* @return array
*/
- public function getAvailableCardTypes()
+ public function getAvailableCardTypes($storeId = null)
{
- $ccTypes = $this->getValue(self::KEY_CC_TYPES);
+ $ccTypes = $this->getValue(self::KEY_CC_TYPES, $storeId);
return !empty($ccTypes) ? explode(',', $ccTypes) : [];
}
@@ -108,79 +110,111 @@ public function getCcTypesMapper()
}
/**
- * Get list of card types available for country
+ * Gets list of card types available for country.
+ *
* @param string $country
+ * @param int|null $storeId
* @return array
*/
- public function getCountryAvailableCardTypes($country)
+ public function getCountryAvailableCardTypes($country, $storeId = null)
{
- $types = $this->getCountrySpecificCardTypeConfig();
+ $types = $this->getCountrySpecificCardTypeConfig($storeId);
return (!empty($types[$country])) ? $types[$country] : [];
}
/**
- * Check if cvv field is enabled
- * @return boolean
+ * Checks if cvv field is enabled.
+ *
+ * @param int|null $storeId
+ * @return bool
*/
- public function isCvvEnabled()
+ public function isCvvEnabled($storeId = null)
{
- return (bool) $this->getValue(self::KEY_USE_CVV);
+ return (bool) $this->getValue(self::KEY_USE_CVV, $storeId);
}
/**
- * Check if 3d secure verification enabled
+ * Checks if 3d secure verification enabled.
+ *
+ * @param int|null $storeId
* @return bool
*/
- public function isVerify3DSecure()
+ public function isVerify3DSecure($storeId = null)
{
- return (bool) $this->getValue(self::KEY_VERIFY_3DSECURE);
+ return (bool) $this->getValue(self::KEY_VERIFY_3DSECURE, $storeId);
}
/**
- * Get threshold amount for 3d secure
+ * Gets threshold amount for 3d secure.
+ *
+ * @param int|null $storeId
* @return float
*/
- public function getThresholdAmount()
+ public function getThresholdAmount($storeId = null)
{
- return (double) $this->getValue(self::KEY_THRESHOLD_AMOUNT);
+ return (double) $this->getValue(self::KEY_THRESHOLD_AMOUNT, $storeId);
}
/**
- * Get list of specific countries for 3d secure
+ * Gets list of specific countries for 3d secure.
+ *
+ * @param int|null $storeId
* @return array
*/
- public function get3DSecureSpecificCountries()
+ public function get3DSecureSpecificCountries($storeId = null)
{
- if ((int) $this->getValue(self::KEY_VERIFY_ALLOW_SPECIFIC) == self::VALUE_3DSECURE_ALL) {
+ if ((int) $this->getValue(self::KEY_VERIFY_ALLOW_SPECIFIC, $storeId) == self::VALUE_3DSECURE_ALL) {
return [];
}
- return explode(',', $this->getValue(self::KEY_VERIFY_SPECIFIC));
+ return explode(',', $this->getValue(self::KEY_VERIFY_SPECIFIC, $storeId));
+ }
+
+ /**
+ * Gets value of configured environment.
+ * Possible values: production or sandbox.
+ *
+ * @param int|null $storeId
+ * @return string
+ */
+ public function getEnvironment($storeId = null)
+ {
+ return $this->getValue(Config::KEY_ENVIRONMENT, $storeId);
}
/**
+ * Gets Kount merchant ID.
+ *
+ * @param int|null $storeId
* @return string
+ * @internal param null $storeId
*/
- public function getEnvironment()
+ public function getKountMerchantId($storeId = null)
{
- return $this->getValue(Config::KEY_ENVIRONMENT);
+ return $this->getValue(Config::KEY_KOUNT_MERCHANT_ID, $storeId);
}
/**
+ * Gets merchant ID.
+ *
+ * @param int|null $storeId
* @return string
*/
- public function getKountMerchantId()
+ public function getMerchantId($storeId = null)
{
- return $this->getValue(Config::KEY_KOUNT_MERCHANT_ID);
+ return $this->getValue(Config::KEY_MERCHANT_ID, $storeId);
}
/**
+ * Gets Merchant account ID.
+ *
+ * @param int|null $storeId
* @return string
*/
- public function getMerchantId()
+ public function getMerchantAccountId($storeId = null)
{
- return $this->getValue(Config::KEY_MERCHANT_ID);
+ return $this->getValue(self::KEY_MERCHANT_ACCOUNT_ID, $storeId);
}
/**
@@ -192,45 +226,42 @@ public function getSdkUrl()
}
/**
+ * Checks if fraud protection is enabled.
+ *
+ * @param int|null $storeId
* @return bool
*/
- public function hasFraudProtection()
+ public function hasFraudProtection($storeId = null)
{
- return (bool) $this->getValue(Config::FRAUD_PROTECTION);
+ return (bool) $this->getValue(Config::FRAUD_PROTECTION, $storeId);
}
/**
- * Get Payment configuration status
+ * Gets Payment configuration status.
+ *
+ * @param int|null $storeId
* @return bool
*/
- public function isActive()
+ public function isActive($storeId = null)
{
- return (bool) $this->getValue(self::KEY_ACTIVE);
+ return (bool) $this->getValue(self::KEY_ACTIVE, $storeId);
}
/**
- * Get list of configured dynamic descriptors
+ * Gets list of configured dynamic descriptors.
+ *
+ * @param int|null $storeId
* @return array
*/
- public function getDynamicDescriptors()
+ public function getDynamicDescriptors($storeId = null)
{
$values = [];
foreach (self::$dynamicDescriptorKeys as $key) {
- $value = $this->getValue('descriptor_' . $key);
+ $value = $this->getValue('descriptor_' . $key, $storeId);
if (!empty($value)) {
$values[$key] = $value;
}
}
return $values;
}
-
- /**
- * Get Merchant account ID
- *
- * @return string
- */
- public function getMerchantAccountId()
- {
- return $this->getValue(self::KEY_MERCHANT_ACCOUNT_ID);
- }
}
diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php b/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php
index caeaaa7fe45a2..ef35152bf7e95 100644
--- a/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php
+++ b/app/code/Magento/Braintree/Gateway/Http/Client/AbstractTransaction.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Http\Client;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Payment\Gateway\Http\ClientException;
use Magento\Payment\Gateway\Http\ClientInterface;
use Magento\Payment\Gateway\Http\TransferInterface;
@@ -29,22 +29,22 @@ abstract class AbstractTransaction implements ClientInterface
protected $customLogger;
/**
- * @var BraintreeAdapter
+ * @var BraintreeAdapterFactory
*/
- protected $adapter;
+ protected $adapterFactory;
/**
* Constructor
*
* @param LoggerInterface $logger
* @param Logger $customLogger
- * @param BraintreeAdapter $transaction
+ * @param BraintreeAdapterFactory $adapterFactory
*/
- public function __construct(LoggerInterface $logger, Logger $customLogger, BraintreeAdapter $adapter)
+ public function __construct(LoggerInterface $logger, Logger $customLogger, BraintreeAdapterFactory $adapterFactory)
{
$this->logger = $logger;
$this->customLogger = $customLogger;
- $this->adapter = $adapter;
+ $this->adapterFactory = $adapterFactory;
}
/**
diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php
index 180344ab7263f..4c3f1e179d378 100644
--- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php
+++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionRefund.php
@@ -16,9 +16,11 @@ class TransactionRefund extends AbstractTransaction
*/
protected function process(array $data)
{
- return $this->adapter->refund(
- $data['transaction_id'],
- $data[PaymentDataBuilder::AMOUNT]
- );
+ $storeId = $data['store_id'] ?? null;
+ // sending store id and other additional keys are restricted by Braintree API
+ unset($data['store_id']);
+
+ return $this->adapterFactory->create($storeId)
+ ->refund($data['transaction_id'], $data[PaymentDataBuilder::AMOUNT]);
}
}
diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php
index 81c79e522907d..529045b5b18f5 100644
--- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php
+++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSale.php
@@ -15,6 +15,11 @@ class TransactionSale extends AbstractTransaction
*/
protected function process(array $data)
{
- return $this->adapter->sale($data);
+ $storeId = $data['store_id'] ?? null;
+ // sending store id and other additional keys are restricted by Braintree API
+ unset($data['store_id']);
+
+ return $this->adapterFactory->create($storeId)
+ ->sale($data);
}
}
diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php
index 0df5799b54b83..16ebd7a7a00c5 100644
--- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php
+++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionSubmitForSettlement.php
@@ -18,9 +18,11 @@ class TransactionSubmitForSettlement extends AbstractTransaction
*/
protected function process(array $data)
{
- return $this->adapter->submitForSettlement(
- $data[CaptureDataBuilder::TRANSACTION_ID],
- $data[PaymentDataBuilder::AMOUNT]
- );
+ $storeId = $data['store_id'] ?? null;
+ // sending store id and other additional keys are restricted by Braintree API
+ unset($data['store_id']);
+
+ return $this->adapterFactory->create($storeId)
+ ->submitForSettlement($data[CaptureDataBuilder::TRANSACTION_ID], $data[PaymentDataBuilder::AMOUNT]);
}
}
diff --git a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php
index a774065365b47..133c7f0f4062e 100644
--- a/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php
+++ b/app/code/Magento/Braintree/Gateway/Http/Client/TransactionVoid.php
@@ -14,6 +14,11 @@ class TransactionVoid extends AbstractTransaction
*/
protected function process(array $data)
{
- return $this->adapter->void($data['transaction_id']);
+ $storeId = $data['store_id'] ?? null;
+ // sending store id and other additional keys are restricted by Braintree API
+ unset($data['store_id']);
+
+ return $this->adapterFactory->create($storeId)
+ ->void($data['transaction_id']);
}
}
diff --git a/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php
index 9ff65149894f8..f7d3aae823e56 100644
--- a/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/AddressDataBuilder.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Request;
use Magento\Payment\Gateway\Request\BuilderInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
/**
* Class AddressDataBuilder
diff --git a/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php
index c6588cfaca05f..6f3a262d7efb4 100644
--- a/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/CaptureDataBuilder.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Request;
use Magento\Framework\Exception\LocalizedException;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Helper\Formatter;
diff --git a/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php
index 6b03f418c2545..6b3403bcd15c1 100644
--- a/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/CustomerDataBuilder.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Request;
use Magento\Payment\Gateway\Request\BuilderInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
/**
* Class CustomerDataBuilder
diff --git a/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php
index 3794058c2be8c..aac603bfb621a 100644
--- a/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/DescriptorDataBuilder.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Braintree\Gateway\Config\Config;
@@ -24,21 +25,29 @@ class DescriptorDataBuilder implements BuilderInterface
private $config;
/**
- * DescriptorDataBuilder constructor.
+ * @var SubjectReader
+ */
+ private $subjectReader;
+
+ /**
* @param Config $config
+ * @param SubjectReader $subjectReader
*/
- public function __construct(Config $config)
+ public function __construct(Config $config, SubjectReader $subjectReader)
{
$this->config = $config;
+ $this->subjectReader = $subjectReader;
}
/**
* @inheritdoc
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function build(array $buildSubject)
{
- $values = $this->config->getDynamicDescriptors();
+ $paymentDO = $this->subjectReader->readPayment($buildSubject);
+ $order = $paymentDO->getOrder();
+
+ $values = $this->config->getDynamicDescriptors($order->getStoreId());
return !empty($values) ? [self::$descriptorKey => $values] : [];
}
}
diff --git a/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php
index 7eebbca1d4290..8538667778504 100644
--- a/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/KountPaymentDataBuilder.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Request;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Observer\DataAssignObserver;
use Magento\Payment\Gateway\Request\BuilderInterface;
@@ -48,10 +48,12 @@ public function __construct(Config $config, SubjectReader $subjectReader)
public function build(array $buildSubject)
{
$result = [];
- if (!$this->config->hasFraudProtection()) {
+ $paymentDO = $this->subjectReader->readPayment($buildSubject);
+ $order = $paymentDO->getOrder();
+
+ if (!$this->config->hasFraudProtection($order->getStoreId())) {
return $result;
}
- $paymentDO = $this->subjectReader->readPayment($buildSubject);
$payment = $paymentDO->getPayment();
$data = $payment->getAdditionalInformation();
diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php
index cea0f8f1291bb..7d0d9dad0db06 100644
--- a/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/DeviceDataBuilder.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request\PayPal;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Observer\DataAssignObserver;
use Magento\Payment\Gateway\Request\BuilderInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php
index 6f76c3415c31a..a035c84b4cafd 100644
--- a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request\PayPal;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Vault\Model\Ui\VaultConfigProvider;
diff --git a/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php
index a3341c3fc1873..85a0c64451398 100644
--- a/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/PaymentDataBuilder.php
@@ -7,7 +7,7 @@
use Magento\Braintree\Gateway\Config\Config;
use Magento\Braintree\Observer\DataAssignObserver;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Helper\Formatter;
@@ -87,7 +87,7 @@ public function build(array $buildSubject)
self::ORDER_ID => $order->getOrderIncrementId()
];
- $merchantAccountId = $this->config->getMerchantAccountId();
+ $merchantAccountId = $this->config->getMerchantAccountId($order->getStoreId());
if (!empty($merchantAccountId)) {
$result[self::MERCHANT_ACCOUNT_ID] = $merchantAccountId;
}
diff --git a/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php
index 82de8e84cbfea..1c25646311160 100644
--- a/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/RefundDataBuilder.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Helper\Formatter;
use Magento\Sales\Api\Data\TransactionInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php b/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php
new file mode 100644
index 0000000000000..014df33690fa0
--- /dev/null
+++ b/app/code/Magento/Braintree/Gateway/Request/StoreConfigBuilder.php
@@ -0,0 +1,42 @@
+subjectReader = $subjectReader;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function build(array $buildSubject)
+ {
+ $paymentDO = $this->subjectReader->readPayment($buildSubject);
+ $order = $paymentDO->getOrder();
+
+ return [
+ 'store_id' => $order->getStoreId()
+ ];
+ }
+}
diff --git a/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php
index c366a67701521..520aa58457753 100644
--- a/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/ThreeDSecureDataBuilder.php
@@ -7,7 +7,7 @@
use Magento\Braintree\Gateway\Config\Config;
use Magento\Payment\Gateway\Data\OrderAdapterInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Helper\Formatter;
@@ -64,12 +64,15 @@ public function build(array $buildSubject)
*/
private function is3DSecureEnabled(OrderAdapterInterface $order, $amount)
{
- if (!$this->config->isVerify3DSecure() || $amount < $this->config->getThresholdAmount()) {
+ $storeId = $order->getStoreId();
+ if (!$this->config->isVerify3DSecure($storeId)
+ || $amount < $this->config->getThresholdAmount($storeId)
+ ) {
return false;
}
$billingAddress = $order->getBillingAddress();
- $specificCounties = $this->config->get3DSecureSpecificCountries();
+ $specificCounties = $this->config->get3DSecureSpecificCountries($storeId);
if (!empty($specificCounties) && !in_array($billingAddress->getCountryId(), $specificCounties)) {
return false;
}
diff --git a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php
index 7182f87e082d1..4280663178efb 100644
--- a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Helper\Formatter;
diff --git a/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php
index 2328ea10f78c7..0bbda28cd344b 100644
--- a/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php
+++ b/app/code/Magento/Braintree/Gateway/Request/VoidDataBuilder.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Request;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php
index 2715da5d3c419..e89e604867baa 100644
--- a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Response;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Helper\ContextHelper;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Api\Data\OrderPaymentInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php
index 766b385851ab8..f527e9051935b 100644
--- a/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/PayPal/VaultDetailsHandler.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Response\PayPal;
use Braintree\Transaction;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Framework\Intl\DateTimeFactory;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Payment\Model\InfoInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php
index 83d7e44a6b612..97bb312af4bd4 100644
--- a/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/PayPalDetailsHandler.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Response;
use Magento\Payment\Gateway\Response\HandlerInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Sales\Api\Data\OrderPaymentInterface;
/**
diff --git a/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php
index 3941640aaeeb1..6e509fae35fec 100644
--- a/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/PaymentDetailsHandler.php
@@ -8,7 +8,7 @@
use Braintree\Transaction;
use Magento\Braintree\Observer\DataAssignObserver;
use Magento\Payment\Gateway\Helper\ContextHelper;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Api\Data\OrderPaymentInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php b/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php
index 95660e10b394c..d4976ff18e0ee 100644
--- a/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/RiskDataHandler.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Gateway\Response;
use Magento\Payment\Gateway\Helper\ContextHelper;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
/**
diff --git a/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php
index 93f37cb561feb..8d61660f03ce5 100644
--- a/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/ThreeDSecureDetailsHandler.php
@@ -7,7 +7,7 @@
use Braintree\Transaction;
use Magento\Payment\Gateway\Helper\ContextHelper;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Api\Data\OrderPaymentInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php b/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php
index 7dd79143736e5..18888bdcf3d4a 100644
--- a/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/TransactionIdHandler.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Gateway\Response;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php
index a8332b9409f78..89bf7f14692b0 100644
--- a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php
+++ b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php
@@ -7,7 +7,7 @@
use Braintree\Transaction;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Payment\Model\InfoInterface;
use Magento\Sales\Api\Data\OrderPaymentExtensionInterface;
diff --git a/app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php b/app/code/Magento/Braintree/Gateway/SubjectReader.php
similarity index 92%
rename from app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php
rename to app/code/Magento/Braintree/Gateway/SubjectReader.php
index e98597655190f..d5dc43a4c5e34 100644
--- a/app/code/Magento/Braintree/Gateway/Helper/SubjectReader.php
+++ b/app/code/Magento/Braintree/Gateway/SubjectReader.php
@@ -3,13 +3,12 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-namespace Magento\Braintree\Gateway\Helper;
+namespace Magento\Braintree\Gateway;
use Braintree\Transaction;
-use Magento\Quote\Model\Quote;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Payment\Gateway\Helper;
use Magento\Vault\Api\Data\PaymentTokenInterface;
-use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
/**
* Class SubjectReader
@@ -119,4 +118,15 @@ public function readPayPal(Transaction $transaction)
return $transaction->paypal;
}
+
+ /**
+ * Reads store's ID, otherwise returns null.
+ *
+ * @param array $subject
+ * @return int|null
+ */
+ public function readStoreId(array $subject)
+ {
+ return $subject['store_id'] ?? null;
+ }
}
diff --git a/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php b/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php
index a70bc2d02e0e5..8028bace0cf24 100644
--- a/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php
+++ b/app/code/Magento/Braintree/Gateway/Validator/GeneralResponseValidator.php
@@ -8,7 +8,7 @@
use Braintree\Result\Error;
use Braintree\Result\Successful;
use Magento\Payment\Gateway\Validator\AbstractValidator;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
class GeneralResponseValidator extends AbstractValidator
diff --git a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php
index 71f3828ebe9d4..d11be39a9bb49 100644
--- a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php
+++ b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapter.php
@@ -15,29 +15,40 @@
/**
* Class BraintreeAdapter
+ * Use \Magento\Braintree\Model\Adapter\BraintreeAdapterFactory to create new instance of adapter.
* @codeCoverageIgnore
*/
class BraintreeAdapter
{
-
/**
* @var Config
*/
private $config;
/**
- * @param Config $config
+ * @param string $merchantId
+ * @param string $publicKey
+ * @param string $privateKey
+ * @param string $environment
*/
- public function __construct(Config $config)
+ public function __construct($merchantId, $publicKey, $privateKey, $environment)
{
- $this->config = $config;
- $this->initCredentials();
+ $this->merchantId($merchantId);
+ $this->publicKey($publicKey);
+ $this->privateKey($privateKey);
+
+ if ($environment == Environment::ENVIRONMENT_PRODUCTION) {
+ $this->environment(Environment::ENVIRONMENT_PRODUCTION);
+ } else {
+ $this->environment(Environment::ENVIRONMENT_SANDBOX);
+ }
}
/**
* Initializes credentials.
*
* @return void
+ * @deprecated is not used anymore
*/
protected function initCredentials()
{
diff --git a/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php
new file mode 100644
index 0000000000000..890f1a1021eda
--- /dev/null
+++ b/app/code/Magento/Braintree/Model/Adapter/BraintreeAdapterFactory.php
@@ -0,0 +1,56 @@
+config = $config;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates instance of Braintree Adapter.
+ *
+ * @param int $storeId if null is provided as an argument, then current scope will be resolved
+ * by \Magento\Framework\App\Config\ScopeCodeResolver (useful for most cases) but for adminhtml area the store
+ * should be provided as the argument for correct config settings loading.
+ * @return BraintreeAdapter
+ */
+ public function create($storeId = null)
+ {
+ return $this->objectManager->create(
+ BraintreeAdapter::class,
+ [
+ 'merchantId' => $this->config->getMerchantId($storeId),
+ 'publicKey' => $this->config->getValue(Config::KEY_PUBLIC_KEY, $storeId),
+ 'privateKey' => $this->config->getValue(Config::KEY_PRIVATE_KEY, $storeId),
+ 'environment' => $this->config->getEnvironment($storeId)
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php b/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php
index edac6e3533849..a237b64bf58ee 100644
--- a/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php
+++ b/app/code/Magento/Braintree/Model/Report/TransactionsCollection.php
@@ -5,7 +5,8 @@
*/
namespace Magento\Braintree\Model\Report;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
+use Magento\Braintree\Model\Report\Row\TransactionMap;
use Magento\Framework\Api\Search\SearchResultInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Data\Collection;
@@ -26,7 +27,7 @@ class TransactionsCollection extends Collection implements SearchResultInterface
*
* @var string
*/
- protected $_itemObjectClass = \Magento\Braintree\Model\Report\Row\TransactionMap::class;
+ protected $_itemObjectClass = TransactionMap::class;
/**
* @var array
@@ -39,9 +40,9 @@ class TransactionsCollection extends Collection implements SearchResultInterface
private $filterMapper;
/**
- * @var BraintreeAdapter
+ * @var BraintreeAdapterFactory
*/
- private $braintreeAdapter;
+ private $braintreeAdapterFactory;
/**
* @var \Braintree\ResourceCollection | null
@@ -50,17 +51,17 @@ class TransactionsCollection extends Collection implements SearchResultInterface
/**
* @param EntityFactoryInterface $entityFactory
- * @param BraintreeAdapter $braintreeAdapter
+ * @param BraintreeAdapterFactory $braintreeAdapterFactory
* @param FilterMapper $filterMapper
*/
public function __construct(
EntityFactoryInterface $entityFactory,
- BraintreeAdapter $braintreeAdapter,
+ BraintreeAdapterFactory $braintreeAdapterFactory,
FilterMapper $filterMapper
) {
parent::__construct($entityFactory);
$this->filterMapper = $filterMapper;
- $this->braintreeAdapter = $braintreeAdapter;
+ $this->braintreeAdapterFactory = $braintreeAdapterFactory;
}
/**
@@ -110,7 +111,8 @@ protected function fetchIdsCollection()
// Fetch all transaction IDs in order to filter
if (empty($this->collection)) {
$filters = $this->getFilters();
- $this->collection = $this->braintreeAdapter->search($filters);
+ $this->collection = $this->braintreeAdapterFactory->create()
+ ->search($filters);
}
return $this->collection;
diff --git a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php
index c420195446270..6ab69923760ce 100644
--- a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php
+++ b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php
@@ -5,10 +5,11 @@
*/
namespace Magento\Braintree\Model\Ui;
+use Magento\Braintree\Gateway\Config\Config;
use Magento\Braintree\Gateway\Request\PaymentDataBuilder;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Checkout\Model\ConfigProviderInterface;
-use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Framework\Session\SessionManagerInterface;
/**
* Class ConfigProvider
@@ -25,27 +26,35 @@ class ConfigProvider implements ConfigProviderInterface
private $config;
/**
- * @var BraintreeAdapter
+ * @var BraintreeAdapterFactory
*/
- private $adapter;
+ private $adapterFactory;
/**
* @var string
*/
private $clientToken = '';
+ /**
+ * @var SessionManagerInterface
+ */
+ private $session;
+
/**
* Constructor
*
* @param Config $config
- * @param BraintreeAdapter $adapter
+ * @param BraintreeAdapterFactory $adapterFactory
+ * @param SessionManagerInterface $session
*/
public function __construct(
Config $config,
- BraintreeAdapter $adapter
+ BraintreeAdapterFactory $adapterFactory,
+ SessionManagerInterface $session
) {
$this->config = $config;
- $this->adapter = $adapter;
+ $this->adapterFactory = $adapterFactory;
+ $this->session = $session;
}
/**
@@ -55,26 +64,27 @@ public function __construct(
*/
public function getConfig()
{
+ $storeId = $this->session->getStoreId();
return [
'payment' => [
self::CODE => [
- 'isActive' => $this->config->isActive(),
+ 'isActive' => $this->config->isActive($storeId),
'clientToken' => $this->getClientToken(),
'ccTypesMapper' => $this->config->getCctypesMapper(),
'sdkUrl' => $this->config->getSdkUrl(),
- 'countrySpecificCardTypes' => $this->config->getCountrySpecificCardTypeConfig(),
- 'availableCardTypes' => $this->config->getAvailableCardTypes(),
- 'useCvv' => $this->config->isCvvEnabled(),
- 'environment' => $this->config->getEnvironment(),
- 'kountMerchantId' => $this->config->getKountMerchantId(),
- 'hasFraudProtection' => $this->config->hasFraudProtection(),
- 'merchantId' => $this->config->getMerchantId(),
+ 'countrySpecificCardTypes' => $this->config->getCountrySpecificCardTypeConfig($storeId),
+ 'availableCardTypes' => $this->config->getAvailableCardTypes($storeId),
+ 'useCvv' => $this->config->isCvvEnabled($storeId),
+ 'environment' => $this->config->getEnvironment($storeId),
+ 'kountMerchantId' => $this->config->getKountMerchantId($storeId),
+ 'hasFraudProtection' => $this->config->hasFraudProtection($storeId),
+ 'merchantId' => $this->config->getMerchantId($storeId),
'ccVaultCode' => self::CC_VAULT_CODE
],
Config::CODE_3DSECURE => [
- 'enabled' => $this->config->isVerify3DSecure(),
- 'thresholdAmount' => $this->config->getThresholdAmount(),
- 'specificCountries' => $this->config->get3DSecureSpecificCountries()
+ 'enabled' => $this->config->isVerify3DSecure($storeId),
+ 'thresholdAmount' => $this->config->getThresholdAmount($storeId),
+ 'specificCountries' => $this->config->get3DSecureSpecificCountries($storeId)
],
]
];
@@ -89,12 +99,14 @@ public function getClientToken()
if (empty($this->clientToken)) {
$params = [];
- $merchantAccountId = $this->config->getMerchantAccountId();
+ $storeId = $this->session->getStoreId();
+ $merchantAccountId = $this->config->getMerchantAccountId($storeId);
if (!empty($merchantAccountId)) {
$params[PaymentDataBuilder::MERCHANT_ACCOUNT_ID] = $merchantAccountId;
}
- $this->clientToken = $this->adapter->generate($params);
+ $this->clientToken = $this->adapterFactory->create($storeId)
+ ->generate($params);
}
return $this->clientToken;
diff --git a/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php b/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php
index 5b28e6d8aedcd..9d363c77f2b05 100644
--- a/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Block/FormTest.php
@@ -13,8 +13,6 @@
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Payment\Helper\Data;
use Magento\Payment\Model\Config;
-use Magento\Store\Api\Data\StoreInterface;
-use Magento\Store\Model\StoreManagerInterface;
use Magento\Vault\Model\VaultPaymentInterface;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
@@ -57,11 +55,6 @@ class FormTest extends \PHPUnit\Framework\TestCase
*/
private $ccType;
- /**
- * @var StoreManagerInterface|MockObject
- */
- private $storeManager;
-
/**
* @var Data|MockObject
*/
@@ -72,8 +65,7 @@ protected function setUp()
$this->initCcTypeMock();
$this->initSessionQuoteMock();
$this->initGatewayConfigMock();
-
- $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class);
+
$this->paymentDataHelper = $this->getMockBuilder(Data::class)
->disableOriginalConstructor()
->setMethods(['getMethodInstance'])
@@ -84,15 +76,13 @@ protected function setUp()
'paymentConfig' => $managerHelper->getObject(Config::class),
'sessionQuote' => $this->sessionQuote,
'gatewayConfig' => $this->gatewayConfig,
- 'ccType' => $this->ccType,
- 'storeManager' => $this->storeManager
+ 'ccType' => $this->ccType
]);
$managerHelper->setBackwardCompatibleProperty($this->block, 'paymentDataHelper', $this->paymentDataHelper);
}
/**
- * @covers \Magento\Braintree\Block\Form::getCcAvailableTypes
* @param string $countryId
* @param array $availableTypes
* @param array $expected
@@ -100,21 +90,18 @@ protected function setUp()
*/
public function testGetCcAvailableTypes($countryId, array $availableTypes, array $expected)
{
- $this->sessionQuote->expects(static::once())
- ->method('getCountryId')
+ $this->sessionQuote->method('getCountryId')
->willReturn($countryId);
- $this->gatewayConfig->expects(static::once())
- ->method('getAvailableCardTypes')
+ $this->gatewayConfig->method('getAvailableCardTypes')
->willReturn(self::$configCardTypes);
- $this->gatewayConfig->expects(static::once())
- ->method('getCountryAvailableCardTypes')
+ $this->gatewayConfig->method('getCountryAvailableCardTypes')
->with($countryId)
->willReturn($availableTypes);
$result = $this->block->getCcAvailableTypes();
- static::assertEquals($expected, array_values($result));
+ self::assertEquals($expected, array_values($result));
}
/**
@@ -131,33 +118,20 @@ public function countryCardTypesDataProvider()
];
}
- /**
- * @covers \Magento\Braintree\Block\Form::isVaultEnabled
- */
public function testIsVaultEnabled()
{
$storeId = 1;
- $store = $this->getMockForAbstractClass(StoreInterface::class);
- $this->storeManager->expects(static::once())
- ->method('getStore')
- ->willReturn($store);
-
- $store->expects(static::once())
- ->method('getId')
- ->willReturn($storeId);
$vaultPayment = $this->getMockForAbstractClass(VaultPaymentInterface::class);
- $this->paymentDataHelper->expects(static::once())
- ->method('getMethodInstance')
+ $this->paymentDataHelper->method('getMethodInstance')
->with(ConfigProvider::CC_VAULT_CODE)
->willReturn($vaultPayment);
- $vaultPayment->expects(static::once())
- ->method('isActive')
- ->with($storeId)
+ $vaultPayment->method('isActive')
+ ->with(self::equalTo($storeId))
->willReturn(true);
- static::assertTrue($this->block->isVaultEnabled());
+ self::assertTrue($this->block->isVaultEnabled());
}
/**
@@ -170,8 +144,7 @@ private function initCcTypeMock()
->setMethods(['getCcTypeLabelMap'])
->getMock();
- $this->ccType->expects(static::any())
- ->method('getCcTypeLabelMap')
+ $this->ccType->method('getCcTypeLabelMap')
->willReturn(self::$baseCardTypes);
}
@@ -182,15 +155,15 @@ private function initSessionQuoteMock()
{
$this->sessionQuote = $this->getMockBuilder(Quote::class)
->disableOriginalConstructor()
- ->setMethods(['getQuote', 'getBillingAddress', 'getCountryId', '__wakeup'])
+ ->setMethods(['getQuote', 'getBillingAddress', 'getCountryId', 'getStoreId'])
->getMock();
- $this->sessionQuote->expects(static::any())
- ->method('getQuote')
+ $this->sessionQuote->method('getQuote')
->willReturnSelf();
- $this->sessionQuote->expects(static::any())
- ->method('getBillingAddress')
+ $this->sessionQuote->method('getBillingAddress')
->willReturnSelf();
+ $this->sessionQuote->method('getStoreId')
+ ->willReturn(1);
}
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php
index e78e54f011d44..723897491da63 100644
--- a/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Controller/Payment/GetNonceTest.php
@@ -16,6 +16,7 @@
use Magento\Framework\Webapi\Exception;
use Magento\Payment\Gateway\Command\ResultInterface as CommandResultInterface;
use Psr\Log\LoggerInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class GetNonceTest
@@ -30,37 +31,37 @@ class GetNonceTest extends \PHPUnit\Framework\TestCase
private $action;
/**
- * @var GetPaymentNonceCommand|\PHPUnit_Framework_MockObject_MockObject
+ * @var GetPaymentNonceCommand|MockObject
*/
private $command;
/**
- * @var Session|\PHPUnit_Framework_MockObject_MockObject
+ * @var Session|MockObject
*/
private $session;
/**
- * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var LoggerInterface|MockObject
*/
private $logger;
/**
- * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var ResultFactory|MockObject
*/
private $resultFactory;
/**
- * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var ResultInterface|MockObject
*/
private $result;
/**
- * @var Http|\PHPUnit_Framework_MockObject_MockObject
+ * @var Http|MockObject
*/
private $request;
/**
- * @var CommandResultInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var CommandResultInterface|MockObject
*/
private $commandResult;
@@ -84,7 +85,7 @@ protected function setUp()
$this->session = $this->getMockBuilder(Session::class)
->disableOriginalConstructor()
- ->setMethods(['getCustomerId'])
+ ->setMethods(['getCustomerId', 'getStoreId'])
->getMock();
$this->logger = $this->createMock(LoggerInterface::class);
@@ -92,11 +93,9 @@ protected function setUp()
$context = $this->getMockBuilder(Context::class)
->disableOriginalConstructor()
->getMock();
- $context->expects(static::any())
- ->method('getRequest')
+ $context->method('getRequest')
->willReturn($this->request);
- $context->expects(static::any())
- ->method('getResultFactory')
+ $context->method('getResultFactory')
->willReturn($this->resultFactory);
$managerHelper = new ObjectManager($this);
@@ -108,81 +107,68 @@ protected function setUp()
]);
}
- /**
- * @covers \Magento\Braintree\Controller\Payment\GetNonce::execute
- */
public function testExecuteWithException()
{
- $this->request->expects(static::once())
- ->method('getParam')
+ $this->request->method('getParam')
->with('public_hash')
->willReturn(null);
- $this->session->expects(static::once())
- ->method('getCustomerId')
+ $this->session->method('getCustomerId')
+ ->willReturn(null);
+ $this->session->method('getStoreId')
->willReturn(null);
$exception = new \Exception('The "publicHash" field does not exists');
- $this->command->expects(static::once())
- ->method('execute')
+ $this->command->method('execute')
->willThrowException($exception);
- $this->logger->expects(static::once())
- ->method('critical')
+ $this->logger->method('critical')
->with($exception);
- $this->result->expects(static::once())
- ->method('setHttpResponseCode')
+ $this->result->method('setHttpResponseCode')
->with(Exception::HTTP_BAD_REQUEST);
- $this->result->expects(static::once())
- ->method('setData')
+ $this->result->method('setData')
->with(['message' => 'Sorry, but something went wrong']);
$this->action->execute();
}
- /**
- * @covers \Magento\Braintree\Controller\Payment\GetNonce::execute
- */
public function testExecute()
{
$customerId = 1;
$publicHash = '65b7bae0dcb690d93';
$nonce = 'f1hc45';
- $this->request->expects(static::once())
- ->method('getParam')
+ $this->request->method('getParam')
->with('public_hash')
->willReturn($publicHash);
- $this->session->expects(static::once())
- ->method('getCustomerId')
+ $this->session->method('getCustomerId')
->willReturn($customerId);
+ $this->session->method('getStoreId')
+ ->willReturn(null);
- $this->commandResult->expects(static::once())
- ->method('get')
+ $this->commandResult->method('get')
->willReturn([
'paymentMethodNonce' => $nonce
]);
- $this->command->expects(static::once())
- ->method('execute')
+ $this->command->method('execute')
->willReturn($this->commandResult);
- $this->result->expects(static::once())
- ->method('setData')
+ $this->result->method('setData')
->with(['paymentMethodNonce' => $nonce]);
- $this->logger->expects(static::never())
+ $this->logger->expects(self::never())
->method('critical');
- $this->result->expects(static::never())
+ $this->result->expects(self::never())
->method('setHttpResponseCode');
$this->action->execute();
}
/**
- * Create mock for result factory object
+ * Creates mock for result factory object
*/
private function initResultFactoryMock()
{
@@ -195,8 +181,7 @@ private function initResultFactoryMock()
->setMethods(['create'])
->getMock();
- $this->resultFactory->expects(static::once())
- ->method('create')
+ $this->resultFactory->method('create')
->willReturn($this->result);
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php
index 56ea1f97fa165..52e17c4ff5d55 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php
@@ -6,23 +6,22 @@
namespace Magento\Braintree\Test\Unit\Gateway\Command;
use Braintree\IsNode;
-use Braintree\MultipleValueNode;
-use Braintree\TextNode;
use Magento\Braintree\Gateway\Command\CaptureStrategyCommand;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
+use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
+use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\Search\SearchCriteria;
use Magento\Framework\Api\SearchCriteriaBuilder;
-use Magento\Payment\Gateway\Command;
use Magento\Payment\Gateway\Command\CommandPoolInterface;
use Magento\Payment\Gateway\Command\GatewayCommand;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Sales\Api\TransactionRepositoryInterface;
use Magento\Sales\Model\Order\Payment;
-use Magento\Sales\Model\Order\Payment\Transaction;
use Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\CollectionFactory;
-use Magento\Braintree\Model\Adapter\BraintreeAdapter;
-use Magento\Braintree\Model\Adapter\BraintreeSearchAdapter;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class CaptureStrategyCommandTest
@@ -37,42 +36,42 @@ class CaptureStrategyCommandTest extends \PHPUnit\Framework\TestCase
private $strategyCommand;
/**
- * @var CommandPoolInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var CommandPoolInterface|MockObject
*/
private $commandPool;
/**
- * @var TransactionRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var TransactionRepositoryInterface|MockObject
*/
private $transactionRepository;
/**
- * @var FilterBuilder|\PHPUnit_Framework_MockObject_MockObject
+ * @var FilterBuilder|MockObject
*/
private $filterBuilder;
/**
- * @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject
+ * @var SearchCriteriaBuilder|MockObject
*/
private $searchCriteriaBuilder;
/**
- * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Payment|MockObject
*/
private $payment;
/**
- * @var GatewayCommand|\PHPUnit_Framework_MockObject_MockObject
+ * @var GatewayCommand|MockObject
*/
private $command;
/**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
+ * @var SubjectReader|MockObject
*/
- private $subjectReaderMock;
+ private $subjectReader;
/**
- * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapter|MockObject
*/
private $braintreeAdapter;
@@ -88,7 +87,7 @@ protected function setUp()
->setMethods(['get', '__wakeup'])
->getMock();
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
+ $this->subjectReader = $this->getMockBuilder(SubjectReader::class)
->disableOriginalConstructor()
->getMock();
@@ -100,6 +99,13 @@ protected function setUp()
$this->braintreeAdapter = $this->getMockBuilder(BraintreeAdapter::class)
->disableOriginalConstructor()
->getMock();
+ /** @var BraintreeAdapterFactory|MockObject $adapterFactory */
+ $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $adapterFactory->method('create')
+ ->willReturn($this->braintreeAdapter);
+
$this->braintreeSearchAdapter = new BraintreeSearchAdapter();
$this->strategyCommand = new CaptureStrategyCommand(
@@ -107,86 +113,68 @@ protected function setUp()
$this->transactionRepository,
$this->filterBuilder,
$this->searchCriteriaBuilder,
- $this->subjectReaderMock,
- $this->braintreeAdapter,
+ $this->subjectReader,
+ $adapterFactory,
$this->braintreeSearchAdapter
);
}
- /**
- * @covers \Magento\Braintree\Gateway\Command\CaptureStrategyCommand::execute
- */
public function testSaleExecute()
{
$paymentData = $this->getPaymentDataObjectMock();
$subject['payment'] = $paymentData;
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
+ $this->subjectReader->method('readPayment')
->with($subject)
->willReturn($paymentData);
- $this->payment->expects(static::once())
- ->method('getAuthorizationTransaction')
+ $this->payment->method('getAuthorizationTransaction')
->willReturn(false);
- $this->payment->expects(static::once())
- ->method('getId')
+ $this->payment->method('getId')
->willReturn(1);
$this->buildSearchCriteria();
- $this->transactionRepository->expects(static::once())
- ->method('getTotalCount')
+ $this->transactionRepository->method('getTotalCount')
->willReturn(0);
- $this->commandPool->expects(static::once())
- ->method('get')
+ $this->commandPool->method('get')
->with(CaptureStrategyCommand::SALE)
->willReturn($this->command);
$this->strategyCommand->execute($subject);
}
- /**
- * @covers \Magento\Braintree\Gateway\Command\CaptureStrategyCommand::execute
- */
public function testCaptureExecute()
{
$paymentData = $this->getPaymentDataObjectMock();
$subject['payment'] = $paymentData;
$lastTransId = 'txnds';
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
+ $this->subjectReader->method('readPayment')
->with($subject)
->willReturn($paymentData);
- $this->payment->expects(static::once())
- ->method('getAuthorizationTransaction')
+ $this->payment->method('getAuthorizationTransaction')
->willReturn(true);
- $this->payment->expects(static::once())
- ->method('getLastTransId')
+ $this->payment->method('getLastTransId')
->willReturn($lastTransId);
- $this->payment->expects(static::once())
- ->method('getId')
+ $this->payment->method('getId')
->willReturn(1);
$this->buildSearchCriteria();
- $this->transactionRepository->expects(static::once())
- ->method('getTotalCount')
+ $this->transactionRepository->method('getTotalCount')
->willReturn(0);
// authorization transaction was not expired
$collection = $this->getNotExpiredExpectedCollection($lastTransId);
- $collection->expects(static::once())
- ->method('maximumCount')
+ $collection->method('maximumCount')
->willReturn(0);
- $this->commandPool->expects(static::once())
- ->method('get')
+ $this->commandPool->method('get')
->with(CaptureStrategyCommand::CAPTURE)
->willReturn($this->command);
@@ -195,7 +183,7 @@ public function testCaptureExecute()
/**
* @param string $lastTransactionId
- * @return \Braintree\ResourceCollection|\PHPUnit_Framework_MockObject_MockObject
+ * @return \Braintree\ResourceCollection|MockObject
*/
private function getNotExpiredExpectedCollection($lastTransactionId)
{
@@ -208,10 +196,9 @@ private function getNotExpiredExpectedCollection($lastTransactionId)
->disableOriginalConstructor()
->getMock();
- $this->braintreeAdapter->expects(static::once())
- ->method('search')
+ $this->braintreeAdapter->method('search')
->with(
- static::callback(
+ self::callback(
function (array $filters) use ($isExpectations) {
foreach ($filters as $filter) {
/** @var IsNode $filter */
@@ -233,80 +220,62 @@ function (array $filters) use ($isExpectations) {
return $collection;
}
- /**
- * @covers \Magento\Braintree\Gateway\Command\CaptureStrategyCommand::execute
- */
public function testExpiredAuthorizationPerformVaultCaptureExecute()
{
$paymentData = $this->getPaymentDataObjectMock();
$subject['payment'] = $paymentData;
$lastTransId = 'txnds';
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
+ $this->subjectReader->method('readPayment')
->with($subject)
->willReturn($paymentData);
- $this->payment->expects(static::once())
- ->method('getAuthorizationTransaction')
+ $this->payment->method('getAuthorizationTransaction')
->willReturn(true);
- $this->payment->expects(static::once())
- ->method('getLastTransId')
+ $this->payment->method('getLastTransId')
->willReturn($lastTransId);
- $this->payment->expects(static::once())
- ->method('getId')
+ $this->payment->method('getId')
->willReturn(1);
$this->buildSearchCriteria();
- $this->transactionRepository->expects(static::once())
- ->method('getTotalCount')
+ $this->transactionRepository->method('getTotalCount')
->willReturn(0);
// authorization transaction was expired
$collection = $this->getNotExpiredExpectedCollection($lastTransId);
- $collection->expects(static::once())
- ->method('maximumCount')
+ $collection->method('maximumCount')
->willReturn(1);
- $this->commandPool->expects(static::once())
- ->method('get')
+ $this->commandPool->method('get')
->with(CaptureStrategyCommand::VAULT_CAPTURE)
->willReturn($this->command);
$this->strategyCommand->execute($subject);
}
- /**
- * @covers \Magento\Braintree\Gateway\Command\CaptureStrategyCommand::execute
- */
public function testVaultCaptureExecute()
{
$paymentData = $this->getPaymentDataObjectMock();
$subject['payment'] = $paymentData;
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
+ $this->subjectReader->method('readPayment')
->with($subject)
->willReturn($paymentData);
- $this->payment->expects(static::once())
- ->method('getAuthorizationTransaction')
+ $this->payment->method('getAuthorizationTransaction')
->willReturn(true);
- $this->payment->expects(static::once())
- ->method('getId')
+ $this->payment->method('getId')
->willReturn(1);
$this->buildSearchCriteria();
- $this->transactionRepository->expects(static::once())
- ->method('getTotalCount')
+ $this->transactionRepository->method('getTotalCount')
->willReturn(1);
- $this->commandPool->expects(static::once())
- ->method('get')
+ $this->commandPool->method('get')
->with(CaptureStrategyCommand::VAULT_CAPTURE)
->willReturn($this->command);
@@ -314,8 +283,8 @@ public function testVaultCaptureExecute()
}
/**
- * Create mock for payment data object and order payment
- * @return \PHPUnit_Framework_MockObject_MockObject
+ * Creates mock for payment data object and order payment
+ * @return MockObject
*/
private function getPaymentDataObjectMock()
{
@@ -324,19 +293,25 @@ private function getPaymentDataObjectMock()
->getMock();
$mock = $this->getMockBuilder(PaymentDataObject::class)
- ->setMethods(['getPayment'])
+ ->setMethods(['getPayment', 'getOrder'])
->disableOriginalConstructor()
->getMock();
- $mock->expects(static::once())
- ->method('getPayment')
+ $mock->method('getPayment')
->willReturn($this->payment);
+ $order = $this->getMockBuilder(OrderAdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock->method('getOrder')
+ ->willReturn($order);
+
return $mock;
}
/**
- * Create mock for gateway command object
+ * Creates mock for gateway command object
*/
private function initCommandMock()
{
@@ -345,13 +320,12 @@ private function initCommandMock()
->setMethods(['execute'])
->getMock();
- $this->command->expects(static::once())
- ->method('execute')
+ $this->command->method('execute')
->willReturn([]);
}
/**
- * Create mock for filter object
+ * Creates mock for filter object
*/
private function initFilterBuilderMock()
{
@@ -362,27 +336,25 @@ private function initFilterBuilderMock()
}
/**
- * Build search criteria
+ * Builds search criteria
*/
private function buildSearchCriteria()
{
- $this->filterBuilder->expects(static::exactly(2))
+ $this->filterBuilder->expects(self::exactly(2))
->method('setField')
->willReturnSelf();
- $this->filterBuilder->expects(static::exactly(2))
+ $this->filterBuilder->expects(self::exactly(2))
->method('setValue')
->willReturnSelf();
$searchCriteria = new SearchCriteria();
- $this->searchCriteriaBuilder->expects(static::exactly(2))
+ $this->searchCriteriaBuilder->expects(self::exactly(2))
->method('addFilters')
->willReturnSelf();
- $this->searchCriteriaBuilder->expects(static::once())
- ->method('create')
+ $this->searchCriteriaBuilder->method('create')
->willReturn($searchCriteria);
- $this->transactionRepository->expects(static::once())
- ->method('getList')
+ $this->transactionRepository->method('getList')
->with($searchCriteria)
->willReturnSelf();
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php
index 333f29eb29136..1b06fc0d21a27 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Command/GetPaymentNonceCommandTest.php
@@ -6,15 +6,16 @@
namespace Magento\Braintree\Test\Unit\Gateway\Command;
use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator;
use Magento\Braintree\Model\Adapter\BraintreeAdapter;
-use Magento\Payment\Gateway\Command;
-use Magento\Payment\Gateway\Command\Result\ArrayResultFactory;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Payment\Gateway\Command\Result\ArrayResult;
+use Magento\Payment\Gateway\Command\Result\ArrayResultFactory;
use Magento\Payment\Gateway\Validator\ResultInterface;
use Magento\Vault\Model\PaymentToken;
use Magento\Vault\Model\PaymentTokenManagement;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class GetPaymentNonceCommandTest
@@ -29,37 +30,37 @@ class GetPaymentNonceCommandTest extends \PHPUnit\Framework\TestCase
private $command;
/**
- * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapter|MockObject
*/
private $adapter;
/**
- * @var PaymentTokenManagement|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentTokenManagement|MockObject
*/
private $tokenManagement;
/**
- * @var PaymentToken|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentToken|MockObject
*/
private $paymentToken;
/**
- * @var ArrayResultFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var ArrayResultFactory|MockObject
*/
private $resultFactory;
/**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
+ * @var SubjectReader|MockObject
*/
private $subjectReader;
/**
- * @var PaymentNonceResponseValidator|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentNonceResponseValidator|MockObject
*/
private $responseValidator;
/**
- * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var ResultInterface|MockObject
*/
private $validationResult;
@@ -79,6 +80,12 @@ protected function setUp()
->disableOriginalConstructor()
->setMethods(['createNonce'])
->getMock();
+ /** @var BraintreeAdapterFactory|MockObject $adapterFactory */
+ $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $adapterFactory->method('create')
+ ->willReturn($this->adapter);
$this->resultFactory = $this->getMockBuilder(ArrayResultFactory::class)
->disableOriginalConstructor()
@@ -101,7 +108,7 @@ protected function setUp()
$this->command = new GetPaymentNonceCommand(
$this->tokenManagement,
- $this->adapter,
+ $adapterFactory,
$this->resultFactory,
$this->subjectReader,
$this->responseValidator
@@ -109,7 +116,6 @@ protected function setUp()
}
/**
- * @covers \Magento\Braintree\Gateway\Command\GetPaymentNonceCommand::execute
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "publicHash" field does not exists
*/
@@ -117,18 +123,16 @@ public function testExecuteWithExceptionForPublicHash()
{
$exception = new \InvalidArgumentException('The "publicHash" field does not exists');
- $this->subjectReader->expects(static::once())
- ->method('readPublicHash')
+ $this->subjectReader->method('readPublicHash')
->willThrowException($exception);
- $this->subjectReader->expects(static::never())
+ $this->subjectReader->expects(self::never())
->method('readCustomerId');
$this->command->execute([]);
}
/**
- * @covers \Magento\Braintree\Gateway\Command\GetPaymentNonceCommand::execute
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "customerId" field does not exists
*/
@@ -136,23 +140,20 @@ public function testExecuteWithExceptionForCustomerId()
{
$publicHash = '3wv2m24d2er3';
- $this->subjectReader->expects(static::once())
- ->method('readPublicHash')
+ $this->subjectReader->method('readPublicHash')
->willReturn($publicHash);
$exception = new \InvalidArgumentException('The "customerId" field does not exists');
- $this->subjectReader->expects(static::once())
- ->method('readCustomerId')
+ $this->subjectReader->method('readCustomerId')
->willThrowException($exception);
- $this->tokenManagement->expects(static::never())
+ $this->tokenManagement->expects(self::never())
->method('getByPublicHash');
$this->command->execute(['publicHash' => $publicHash]);
}
/**
- * @covers \Magento\Braintree\Gateway\Command\GetPaymentNonceCommand::execute
* @expectedException \Exception
* @expectedExceptionMessage No available payment tokens
*/
@@ -161,27 +162,23 @@ public function testExecuteWithExceptionForTokenManagement()
$publicHash = '3wv2m24d2er3';
$customerId = 1;
- $this->subjectReader->expects(static::once())
- ->method('readPublicHash')
+ $this->subjectReader->method('readPublicHash')
->willReturn($publicHash);
- $this->subjectReader->expects(static::once())
- ->method('readCustomerId')
+ $this->subjectReader->method('readCustomerId')
->willReturn($customerId);
$exception = new \Exception('No available payment tokens');
- $this->tokenManagement->expects(static::once())
- ->method('getByPublicHash')
+ $this->tokenManagement->method('getByPublicHash')
->willThrowException($exception);
- $this->paymentToken->expects(static::never())
+ $this->paymentToken->expects(self::never())
->method('getGatewayToken');
$this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]);
}
/**
- * @covers \Magento\Braintree\Gateway\Command\GetPaymentNonceCommand::execute
* @expectedException \Exception
* @expectedExceptionMessage Payment method nonce can't be retrieved.
*/
@@ -191,52 +188,41 @@ public function testExecuteWithFailedValidation()
$customerId = 1;
$token = 'jd2vnq';
- $this->subjectReader->expects(static::once())
- ->method('readPublicHash')
+ $this->subjectReader->method('readPublicHash')
->willReturn($publicHash);
- $this->subjectReader->expects(static::once())
- ->method('readCustomerId')
+ $this->subjectReader->method('readCustomerId')
->willReturn($customerId);
- $this->tokenManagement->expects(static::once())
- ->method('getByPublicHash')
+ $this->tokenManagement->method('getByPublicHash')
->with($publicHash, $customerId)
->willReturn($this->paymentToken);
- $this->paymentToken->expects(static::once())
- ->method('getGatewayToken')
+ $this->paymentToken->method('getGatewayToken')
->willReturn($token);
$obj = new \stdClass();
$obj->success = false;
- $this->adapter->expects(static::once())
- ->method('createNonce')
+ $this->adapter->method('createNonce')
->with($token)
->willReturn($obj);
- $this->responseValidator->expects(static::once())
- ->method('validate')
+ $this->responseValidator->method('validate')
->with(['response' => ['object' => $obj]])
->willReturn($this->validationResult);
- $this->validationResult->expects(static::once())
- ->method('isValid')
+ $this->validationResult->method('isValid')
->willReturn(false);
- $this->validationResult->expects(static::once())
- ->method('getFailsDescription')
+ $this->validationResult->method('getFailsDescription')
->willReturn(['Payment method nonce can\'t be retrieved.']);
- $this->resultFactory->expects(static::never())
+ $this->resultFactory->expects(self::never())
->method('create');
$this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]);
}
- /**
- * @covers \Magento\Braintree\Gateway\Command\GetPaymentNonceCommand::execute
- */
public function testExecute()
{
$publicHash = '3wv2m24d2er3';
@@ -244,57 +230,48 @@ public function testExecute()
$token = 'jd2vnq';
$nonce = 's1dj23';
- $this->subjectReader->expects(static::once())
- ->method('readPublicHash')
+ $this->subjectReader->method('readPublicHash')
->willReturn($publicHash);
- $this->subjectReader->expects(static::once())
- ->method('readCustomerId')
+ $this->subjectReader->method('readCustomerId')
->willReturn($customerId);
- $this->tokenManagement->expects(static::once())
- ->method('getByPublicHash')
+ $this->tokenManagement->method('getByPublicHash')
->with($publicHash, $customerId)
->willReturn($this->paymentToken);
- $this->paymentToken->expects(static::once())
- ->method('getGatewayToken')
+ $this->paymentToken->method('getGatewayToken')
->willReturn($token);
$obj = new \stdClass();
$obj->success = true;
$obj->paymentMethodNonce = new \stdClass();
$obj->paymentMethodNonce->nonce = $nonce;
- $this->adapter->expects(static::once())
- ->method('createNonce')
+ $this->adapter->method('createNonce')
->with($token)
->willReturn($obj);
- $this->responseValidator->expects(static::once())
- ->method('validate')
+ $this->responseValidator->method('validate')
->with(['response' => ['object' => $obj]])
->willReturn($this->validationResult);
- $this->validationResult->expects(static::once())
- ->method('isValid')
+ $this->validationResult->method('isValid')
->willReturn(true);
- $this->validationResult->expects(static::never())
+ $this->validationResult->expects(self::never())
->method('getFailsDescription');
$expected = $this->getMockBuilder(ArrayResult::class)
->disableOriginalConstructor()
->setMethods(['get'])
->getMock();
- $expected->expects(static::once())
- ->method('get')
+ $expected->method('get')
->willReturn(['paymentMethodNonce' => $nonce]);
- $this->resultFactory->expects(static::once())
- ->method('create')
+ $this->resultFactory->method('create')
->willReturn($expected);
$actual = $this->command->execute(['publicHash' => $publicHash, 'customerId' => $customerId]);
- static::assertEquals($expected, $actual);
- static::assertEquals($nonce, $actual->get()['paymentMethodNonce']);
+ self::assertEquals($expected, $actual);
+ self::assertEquals($nonce, $actual->get()['paymentMethodNonce']);
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php
index 793700ab1971f..bb258f27455a4 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/CanVoidHandlerTest.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Test\Unit\Gateway\Config;
use Magento\Braintree\Gateway\Config\CanVoidHandler;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Payment\Model\InfoInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php
index b2207563b8b0f..21b8a13be6bed 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Helper/SubjectReaderTest.php
@@ -7,7 +7,7 @@
use Braintree\Transaction;
use InvalidArgumentException;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
/**
* Class SubjectReaderTest
@@ -25,7 +25,7 @@ protected function setUp()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readCustomerId
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId
* @expectedException InvalidArgumentException
* @expectedExceptionMessage The "customerId" field does not exists
*/
@@ -35,7 +35,7 @@ public function testReadCustomerIdWithException()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readCustomerId
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readCustomerId
*/
public function testReadCustomerId()
{
@@ -44,7 +44,7 @@ public function testReadCustomerId()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPublicHash
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash
* @expectedException InvalidArgumentException
* @expectedExceptionMessage The "public_hash" field does not exists
*/
@@ -54,7 +54,7 @@ public function testReadPublicHashWithException()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPublicHash
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readPublicHash
*/
public function testReadPublicHash()
{
@@ -63,7 +63,7 @@ public function testReadPublicHash()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPayPal
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Transaction has't paypal attribute
*/
@@ -76,7 +76,7 @@ public function testReadPayPalWithException()
}
/**
- * @covers \Magento\Braintree\Gateway\Helper\SubjectReader::readPayPal
+ * @covers \Magento\Braintree\Gateway\SubjectReader::readPayPal
*/
public function testReadPayPal()
{
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php
index 5cbcfef8b6b9e..48889f1efd2e5 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSaleTest.php
@@ -7,8 +7,10 @@
use Magento\Braintree\Gateway\Http\Client\TransactionSale;
use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Payment\Gateway\Http\TransferInterface;
use Magento\Payment\Model\Method\Logger;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Psr\Log\LoggerInterface;
/**
@@ -22,35 +24,41 @@ class TransactionSaleTest extends \PHPUnit\Framework\TestCase
private $model;
/**
- * @var Logger|\PHPUnit_Framework_MockObject_MockObject
+ * @var Logger|MockObject
*/
- private $loggerMock;
+ private $logger;
/**
- * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapter|MockObject
*/
private $adapter;
/**
- * Set up
- *
- * @return void
+ * @inheritdoc
*/
protected function setUp()
{
- $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class);
- $this->loggerMock = $this->getMockBuilder(Logger::class)
+ /** @var LoggerInterface|MockObject $criticalLogger */
+ $criticalLogger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->logger = $this->getMockBuilder(Logger::class)
->disableOriginalConstructor()
->getMock();
+
$this->adapter = $this->getMockBuilder(BraintreeAdapter::class)
->disableOriginalConstructor()
->getMock();
+ /** @var BraintreeAdapterFactory|MockObject $adapterFactory */
+ $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $adapterFactory->method('create')
+ ->willReturn($this->adapter);
- $this->model = new TransactionSale($criticalLoggerMock, $this->loggerMock, $this->adapter);
+ $this->model = new TransactionSale($criticalLogger, $this->logger, $adapterFactory);
}
/**
- * Run test placeRequest method (exception)
+ * Runs test placeRequest method (exception)
*
* @return void
*
@@ -59,8 +67,7 @@ protected function setUp()
*/
public function testPlaceRequestException()
{
- $this->loggerMock->expects($this->once())
- ->method('debug')
+ $this->logger->method('debug')
->with(
[
'request' => $this->getTransferData(),
@@ -69,11 +76,10 @@ public function testPlaceRequestException()
]
);
- $this->adapter->expects($this->once())
- ->method('sale')
+ $this->adapter->method('sale')
->willThrowException(new \Exception('Test messages'));
- /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */
+ /** @var TransferInterface|MockObject $transferObjectMock */
$transferObjectMock = $this->getTransferObjectMock();
$this->model->placeRequest($transferObjectMock);
@@ -87,14 +93,11 @@ public function testPlaceRequestException()
public function testPlaceRequestSuccess()
{
$response = $this->getResponseObject();
- $this->adapter->expects($this->once())
- ->method('sale')
+ $this->adapter->method('sale')
->with($this->getTransferData())
- ->willReturn($response)
- ;
+ ->willReturn($response);
- $this->loggerMock->expects($this->once())
- ->method('debug')
+ $this->logger->method('debug')
->with(
[
'request' => $this->getTransferData(),
@@ -110,19 +113,22 @@ public function testPlaceRequestSuccess()
}
/**
- * @return TransferInterface|\PHPUnit_Framework_MockObject_MockObject
+ * Creates mock object for TransferInterface.
+ *
+ * @return TransferInterface|MockObject
*/
private function getTransferObjectMock()
{
$transferObjectMock = $this->createMock(TransferInterface::class);
- $transferObjectMock->expects($this->once())
- ->method('getBody')
+ $transferObjectMock->method('getBody')
->willReturn($this->getTransferData());
return $transferObjectMock;
}
/**
+ * Creates stub for a response.
+ *
* @return \stdClass
*/
private function getResponseObject()
@@ -134,6 +140,8 @@ private function getResponseObject()
}
/**
+ * Creates stub request data.
+ *
* @return array
*/
private function getTransferData()
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php
index 86113c34ba218..09b0efc58f3fb 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Http/Client/TransactionSubmitForSettlementTest.php
@@ -8,8 +8,10 @@
use Braintree\Result\Successful;
use Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement;
use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Payment\Gateway\Http\TransferInterface;
use Magento\Payment\Model\Method\Logger;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Psr\Log\LoggerInterface;
/**
@@ -23,76 +25,79 @@ class TransactionSubmitForSettlementTest extends \PHPUnit\Framework\TestCase
private $client;
/**
- * @var Logger|\PHPUnit_Framework_MockObject_MockObject
+ * @var Logger|MockObject
*/
private $logger;
/**
- * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapter|MockObject
*/
private $adapter;
protected function setUp()
{
- $criticalLoggerMock = $this->getMockForAbstractClass(LoggerInterface::class);
+ /** @var LoggerInterface|MockObject $criticalLogger */
+ $criticalLogger = $this->getMockForAbstractClass(LoggerInterface::class);
$this->logger = $this->getMockBuilder(Logger::class)
->disableOriginalConstructor()
->setMethods(['debug'])
->getMock();
+
$this->adapter = $this->getMockBuilder(BraintreeAdapter::class)
->disableOriginalConstructor()
->setMethods(['submitForSettlement'])
->getMock();
+ /** @var BraintreeAdapterFactory|MockObject $adapterFactory */
+ $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $adapterFactory->method('create')
+ ->willReturn($this->adapter);
$this->client = new TransactionSubmitForSettlement(
- $criticalLoggerMock,
+ $criticalLogger,
$this->logger,
- $this->adapter
+ $adapterFactory
);
}
/**
- * @covers \Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement::placeRequest
* @expectedException \Magento\Payment\Gateway\Http\ClientException
* @expectedExceptionMessage Transaction has been declined
*/
public function testPlaceRequestWithException()
{
$exception = new \Exception('Transaction has been declined');
- $this->adapter->expects(static::once())
- ->method('submitForSettlement')
+ $this->adapter->method('submitForSettlement')
->willThrowException($exception);
- /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */
- $transferObjectMock = $this->getTransferObjectMock();
- $this->client->placeRequest($transferObjectMock);
+ /** @var TransferInterface|MockObject $transferObject */
+ $transferObject = $this->getTransferObjectMock();
+ $this->client->placeRequest($transferObject);
}
- /**
- * @covers \Magento\Braintree\Gateway\Http\Client\TransactionSubmitForSettlement::process
- */
public function testPlaceRequest()
{
$data = new Successful(['success'], [true]);
- $this->adapter->expects(static::once())
- ->method('submitForSettlement')
+ $this->adapter->method('submitForSettlement')
->willReturn($data);
- /** @var TransferInterface|\PHPUnit_Framework_MockObject_MockObject $transferObjectMock */
- $transferObjectMock = $this->getTransferObjectMock();
- $response = $this->client->placeRequest($transferObjectMock);
+ /** @var TransferInterface|MockObject $transferObject */
+ $transferObject = $this->getTransferObjectMock();
+ $response = $this->client->placeRequest($transferObject);
static::assertTrue(is_object($response['object']));
static::assertEquals(['object' => $data], $response);
}
/**
- * @return TransferInterface|\PHPUnit_Framework_MockObject_MockObject
+ * Creates mock for TransferInterface
+ *
+ * @return TransferInterface|MockObject
*/
private function getTransferObjectMock()
{
$mock = $this->createMock(TransferInterface::class);
- $mock->expects($this->once())
- ->method('getBody')
+ $mock->method('getBody')
->willReturn([
'transaction_id' => 'vb4c6b',
'amount' => 124.00
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php
index 3f05aed45da60..702b3254e3085 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/AddressDataBuilderTest.php
@@ -5,11 +5,12 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\AddressDataBuilder;
-use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
-use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Payment\Gateway\Data\AddressAdapterInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class AddressDataBuilderTest
@@ -17,34 +18,26 @@
class AddressDataBuilderTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentDataObjectInterface|MockObject
*/
- private $paymentDOMock;
+ private $paymentDO;
/**
- * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var OrderAdapterInterface|MockObject
*/
- private $orderMock;
+ private $order;
/**
* @var AddressDataBuilder
*/
private $builder;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReaderMock;
-
protected function setUp()
{
- $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class);
- $this->orderMock = $this->createMock(OrderAdapterInterface::class);
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
+ $this->order = $this->createMock(OrderAdapterInterface::class);
- $this->builder = new AddressDataBuilder($this->subjectReaderMock);
+ $this->builder = new AddressDataBuilder(new SubjectReader());
}
/**
@@ -56,37 +49,24 @@ public function testBuildReadPaymentException()
'payment' => null,
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
-
$this->builder->build($buildSubject);
}
public function testBuildNoAddresses()
{
- $this->paymentDOMock->expects(static::once())
- ->method('getOrder')
- ->willReturn($this->orderMock);
+ $this->paymentDO->method('getOrder')
+ ->willReturn($this->order);
- $this->orderMock->expects(static::once())
- ->method('getShippingAddress')
+ $this->order->method('getShippingAddress')
->willReturn(null);
- $this->orderMock->expects(static::once())
- ->method('getBillingAddress')
+ $this->order->method('getBillingAddress')
->willReturn(null);
$buildSubject = [
- 'payment' => $this->paymentDOMock,
+ 'payment' => $this->paymentDO,
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDOMock);
-
- static::assertEquals([], $this->builder->build($buildSubject));
+ self::assertEquals([], $this->builder->build($buildSubject));
}
/**
@@ -97,28 +77,20 @@ public function testBuildNoAddresses()
*/
public function testBuild($addressData, $expectedResult)
{
- $addressMock = $this->getAddressMock($addressData);
+ $address = $this->getAddressMock($addressData);
- $this->paymentDOMock->expects(static::once())
- ->method('getOrder')
- ->willReturn($this->orderMock);
+ $this->paymentDO->method('getOrder')
+ ->willReturn($this->order);
- $this->orderMock->expects(static::once())
- ->method('getShippingAddress')
- ->willReturn($addressMock);
- $this->orderMock->expects(static::once())
- ->method('getBillingAddress')
- ->willReturn($addressMock);
+ $this->order->method('getShippingAddress')
+ ->willReturn($address);
+ $this->order->method('getBillingAddress')
+ ->willReturn($address);
$buildSubject = [
- 'payment' => $this->paymentDOMock,
+ 'payment' => $this->paymentDO,
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDOMock);
-
self::assertEquals($expectedResult, $this->builder->build($buildSubject));
}
@@ -171,37 +143,37 @@ public function dataProviderBuild()
/**
* @param array $addressData
- * @return AddressAdapterInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @return AddressAdapterInterface|MockObject
*/
private function getAddressMock($addressData)
{
$addressMock = $this->createMock(AddressAdapterInterface::class);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getFirstname')
->willReturn($addressData['first_name']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getLastname')
->willReturn($addressData['last_name']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getCompany')
->willReturn($addressData['company']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getStreetLine1')
->willReturn($addressData['street_1']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getStreetLine2')
->willReturn($addressData['street_2']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getCity')
->willReturn($addressData['city']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getRegionCode')
->willReturn($addressData['region_code']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getPostcode')
->willReturn($addressData['post_code']);
- $addressMock->expects(static::exactly(2))
+ $addressMock->expects(self::exactly(2))
->method('getCountryId')
->willReturn($addressData['country_id']);
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php
index 9799b6f18c639..b42b1162b3d70 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php
@@ -8,7 +8,8 @@
use Magento\Braintree\Gateway\Request\CaptureDataBuilder;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Model\Order\Payment;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class CaptureDataBuilderTest
@@ -21,35 +22,26 @@ class CaptureDataBuilderTest extends \PHPUnit\Framework\TestCase
private $builder;
/**
- * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Payment|MockObject
*/
private $payment;
/**
- * @var \Magento\Sales\Model\Order\Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Sales\Model\Order\Payment|MockObject
*/
private $paymentDO;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReaderMock;
-
protected function setUp()
{
$this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
$this->payment = $this->getMockBuilder(Payment::class)
->disableOriginalConstructor()
->getMock();
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->builder = new CaptureDataBuilder($this->subjectReaderMock);
+ $this->builder = new CaptureDataBuilder(new SubjectReader());
}
/**
- * @covers \Magento\Braintree\Gateway\Request\CaptureDataBuilder::build
* @expectedException \Magento\Framework\Exception\LocalizedException
* @expectedExceptionMessage No authorization transaction to proceed capture.
*/
@@ -61,25 +53,15 @@ public function testBuildWithException()
'amount' => $amount
];
- $this->payment->expects(static::once())
- ->method('getCcTransId')
+ $this->payment->method('getCcTransId')
->willReturn('');
- $this->paymentDO->expects(static::once())
- ->method('getPayment')
+ $this->paymentDO->method('getPayment')
->willReturn($this->payment);
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
-
$this->builder->build($buildSubject);
}
- /**
- * @covers \Magento\Braintree\Gateway\Request\CaptureDataBuilder::build
- */
public function testBuild()
{
$transactionId = 'b3b99d';
@@ -95,23 +77,12 @@ public function testBuild()
'amount' => $amount
];
- $this->payment->expects(static::once())
- ->method('getCcTransId')
+ $this->payment->method('getCcTransId')
->willReturn($transactionId);
- $this->paymentDO->expects(static::once())
- ->method('getPayment')
+ $this->paymentDO->method('getPayment')
->willReturn($this->payment);
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
- $this->subjectReaderMock->expects(self::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willReturn($amount);
-
- static::assertEquals($expected, $this->builder->build($buildSubject));
+ self::assertEquals($expected, $this->builder->build($buildSubject));
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php
index 0f25b26fd2fa3..9b5cee30940df 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php
@@ -5,46 +5,36 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\CustomerDataBuilder;
-use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
-use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Payment\Gateway\Data\AddressAdapterInterface;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
-/**
- * Class CustomerDataBuilderTest
- */
class CustomerDataBuilderTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentDataObjectInterface|MockObject
*/
- private $paymentDOMock;
+ private $paymentDO;
/**
- * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var OrderAdapterInterface|MockObject
*/
- private $orderMock;
+ private $order;
/**
* @var CustomerDataBuilder
*/
private $builder;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReaderMock;
-
protected function setUp()
{
- $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class);
- $this->orderMock = $this->createMock(OrderAdapterInterface::class);
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
+ $this->order = $this->createMock(OrderAdapterInterface::class);
- $this->builder = new CustomerDataBuilder($this->subjectReaderMock);
+ $this->builder = new CustomerDataBuilder(new SubjectReader());
}
/**
@@ -56,11 +46,6 @@ public function testBuildReadPaymentException()
'payment' => null,
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
-
$this->builder->build($buildSubject);
}
@@ -74,22 +59,15 @@ public function testBuild($billingData, $expectedResult)
{
$billingMock = $this->getBillingMock($billingData);
- $this->paymentDOMock->expects(static::once())
- ->method('getOrder')
- ->willReturn($this->orderMock);
- $this->orderMock->expects(static::once())
- ->method('getBillingAddress')
+ $this->paymentDO->method('getOrder')
+ ->willReturn($this->order);
+ $this->order->method('getBillingAddress')
->willReturn($billingMock);
$buildSubject = [
- 'payment' => $this->paymentDOMock,
+ 'payment' => $this->paymentDO,
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDOMock);
-
self::assertEquals($expectedResult, $this->builder->build($buildSubject));
}
@@ -122,28 +100,23 @@ public function dataProviderBuild()
/**
* @param array $billingData
- * @return AddressAdapterInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @return AddressAdapterInterface|MockObject
*/
private function getBillingMock($billingData)
{
- $addressMock = $this->createMock(AddressAdapterInterface::class);
+ $address = $this->createMock(AddressAdapterInterface::class);
- $addressMock->expects(static::once())
- ->method('getFirstname')
+ $address->method('getFirstname')
->willReturn($billingData['first_name']);
- $addressMock->expects(static::once())
- ->method('getLastname')
+ $address->method('getLastname')
->willReturn($billingData['last_name']);
- $addressMock->expects(static::once())
- ->method('getCompany')
+ $address->method('getCompany')
->willReturn($billingData['company']);
- $addressMock->expects(static::once())
- ->method('getTelephone')
+ $address->method('getTelephone')
->willReturn($billingData['phone']);
- $addressMock->expects(static::once())
- ->method('getEmail')
+ $address->method('getEmail')
->willReturn($billingData['email']);
- return $addressMock;
+ return $address;
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php
index 761d88b636ed7..eda9121236045 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/DescriptorDataBuilderTest.php
@@ -6,12 +6,12 @@
namespace Magento\Braintree\Test\Unit\Gateway\Request;
use Magento\Braintree\Gateway\Config\Config;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\DescriptorDataBuilder;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
-/**
- * Class DescriptorDataBuilderTest
- */
class DescriptorDataBuilderTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -31,22 +31,25 @@ protected function setUp()
->setMethods(['getDynamicDescriptors'])
->getMock();
- $this->builder = new DescriptorDataBuilder($this->config);
+ $this->builder = new DescriptorDataBuilder($this->config, new SubjectReader());
}
/**
- * @covers \Magento\Braintree\Gateway\Request\DescriptorDataBuilder::build
* @param array $descriptors
* @param array $expected
* @dataProvider buildDataProvider
*/
public function testBuild(array $descriptors, array $expected)
{
- $this->config->expects(static::once())
- ->method('getDynamicDescriptors')
+ $paymentDO = $this->createMock(PaymentDataObjectInterface::class);
+ $order = $this->createMock(OrderAdapterInterface::class);
+ $paymentDO->method('getOrder')
+ ->willReturn($order);
+
+ $this->config->method('getDynamicDescriptors')
->willReturn($descriptors);
- $actual = $this->builder->build([]);
+ $actual = $this->builder->build(['payment' => $paymentDO]);
static::assertEquals($expected, $actual);
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php
index ee0907a1ddbbb..3737089ed175e 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/KountPaymentDataBuilderTest.php
@@ -5,12 +5,14 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request;
+use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Sales\Model\Order\Payment;
use Magento\Braintree\Gateway\Config\Config;
use Magento\Braintree\Observer\DataAssignObserver;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Braintree\Gateway\Request\KountPaymentDataBuilder;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class KountPaymentDataBuilderTest
@@ -27,39 +29,31 @@ class KountPaymentDataBuilderTest extends \PHPUnit\Framework\TestCase
private $builder;
/**
- * @var Config|\PHPUnit_Framework_MockObject_MockObject
+ * @var Config|MockObject
*/
- private $configMock;
+ private $config;
/**
- * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Payment|MockObject
*/
- private $paymentMock;
+ private $payment;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
private $paymentDO;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReaderMock;
-
protected function setUp()
{
$this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
- $this->configMock = $this->getMockBuilder(Config::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->paymentMock = $this->getMockBuilder(Payment::class)
+ $this->config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
+ $this->payment = $this->getMockBuilder(Payment::class)
->disableOriginalConstructor()
->getMock();
- $this->builder = new KountPaymentDataBuilder($this->configMock, $this->subjectReaderMock);
+ $this->builder = new KountPaymentDataBuilder($this->config, new SubjectReader());
}
/**
@@ -69,15 +63,9 @@ public function testBuildReadPaymentException()
{
$buildSubject = [];
- $this->configMock->expects(static::once())
- ->method('hasFraudProtection')
+ $this->config->method('hasFraudProtection')
->willReturn(true);
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
-
$this->builder->build($buildSubject);
}
@@ -91,26 +79,23 @@ public function testBuild()
KountPaymentDataBuilder::DEVICE_DATA => self::DEVICE_DATA,
];
+ $order = $this->createMock(OrderAdapterInterface::class);
+ $this->paymentDO->method('getOrder')
+ ->willReturn($order);
+
$buildSubject = ['payment' => $this->paymentDO];
- $this->paymentMock->expects(static::exactly(count($additionalData)))
+ $this->payment->expects(self::exactly(count($additionalData)))
->method('getAdditionalInformation')
->willReturn($additionalData);
- $this->configMock->expects(static::once())
- ->method('hasFraudProtection')
+ $this->config->method('hasFraudProtection')
->willReturn(true);
- $this->paymentDO->expects(static::once())
- ->method('getPayment')
- ->willReturn($this->paymentMock);
-
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
+ $this->paymentDO->method('getPayment')
+ ->willReturn($this->payment);
- static::assertEquals(
+ self::assertEquals(
$expectedResult,
$this->builder->build($buildSubject)
);
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php
index fba65354d6095..b363f2bcac673 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/DeviceDataBuilderTest.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request\PayPal;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\PayPal\DeviceDataBuilder;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Payment\Model\InfoInterface;
@@ -16,15 +16,10 @@
*/
class DeviceDataBuilderTest extends \PHPUnit\Framework\TestCase
{
- /**
- * @var SubjectReader|MockObject
- */
- private $subjectReader;
-
/**
* @var PaymentDataObjectInterface|MockObject
*/
- private $paymentDataObject;
+ private $paymentDO;
/**
* @var InfoInterface|MockObject
@@ -38,16 +33,10 @@ class DeviceDataBuilderTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->subjectReader = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->setMethods(['readPayment'])
- ->getMock();
-
- $this->paymentDataObject = $this->createMock(PaymentDataObjectInterface::class);
-
+ $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
$this->paymentInfo = $this->createMock(InfoInterface::class);
- $this->builder = new DeviceDataBuilder($this->subjectReader);
+ $this->builder = new DeviceDataBuilder(new SubjectReader());
}
/**
@@ -59,24 +48,17 @@ protected function setUp()
public function testBuild(array $paymentData, array $expected)
{
$subject = [
- 'payment' => $this->paymentDataObject
+ 'payment' => $this->paymentDO
];
- $this->subjectReader->expects(static::once())
- ->method('readPayment')
- ->with($subject)
- ->willReturn($this->paymentDataObject);
-
- $this->paymentDataObject->expects(static::once())
- ->method('getPayment')
+ $this->paymentDO->method('getPayment')
->willReturn($this->paymentInfo);
- $this->paymentInfo->expects(static::once())
- ->method('getAdditionalInformation')
+ $this->paymentInfo->method('getAdditionalInformation')
->willReturn($paymentData);
$actual = $this->builder->build($subject);
- static::assertEquals($expected, $actual);
+ self::assertEquals($expected, $actual);
}
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php
index 8e83254727bf7..10a96734b849a 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PayPal/VaultDataBuilderTest.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request\PayPal;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\PayPal\VaultDataBuilder;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Payment\Model\InfoInterface;
@@ -17,15 +17,10 @@
*/
class VaultDataBuilderTest extends \PHPUnit\Framework\TestCase
{
- /**
- * @var SubjectReader|MockObject
- */
- private $subjectReader;
-
/**
* @var PaymentDataObjectInterface|MockObject
*/
- private $paymentDataObject;
+ private $paymentDO;
/**
* @var InfoInterface|MockObject
@@ -39,16 +34,11 @@ class VaultDataBuilderTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->paymentDataObject = $this->createMock(PaymentDataObjectInterface::class);
+ $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
$this->paymentInfo = $this->createMock(InfoInterface::class);
- $this->subjectReader = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->setMethods(['readPayment'])
- ->getMock();
-
- $this->builder = new VaultDataBuilder($this->subjectReader);
+ $this->builder = new VaultDataBuilder(new SubjectReader());
}
/**
@@ -60,24 +50,17 @@ protected function setUp()
public function testBuild(array $additionalInfo, array $expected)
{
$subject = [
- 'payment' => $this->paymentDataObject
+ 'payment' => $this->paymentDO
];
- $this->subjectReader->expects(static::once())
- ->method('readPayment')
- ->with($subject)
- ->willReturn($this->paymentDataObject);
-
- $this->paymentDataObject->expects(static::once())
- ->method('getPayment')
+ $this->paymentDO->method('getPayment')
->willReturn($this->paymentInfo);
- $this->paymentInfo->expects(static::once())
- ->method('getAdditionalInformation')
+ $this->paymentInfo->method('getAdditionalInformation')
->willReturn($additionalInfo);
$actual = $this->builder->build($subject);
- static::assertEquals($expected, $actual);
+ self::assertEquals($expected, $actual);
}
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php
index 12c613b8f216b..16f11559f73cc 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php
@@ -6,12 +6,13 @@
namespace Magento\Braintree\Test\Unit\Gateway\Request;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\PaymentDataBuilder;
use Magento\Braintree\Observer\DataAssignObserver;
use Magento\Payment\Gateway\Data\OrderAdapterInterface;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Model\Order\Payment;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class PaymentDataBuilderTest
@@ -29,45 +30,37 @@ class PaymentDataBuilderTest extends \PHPUnit\Framework\TestCase
private $builder;
/**
- * @var Config|\PHPUnit_Framework_MockObject_MockObject
+ * @var Config|MockObject
*/
- private $configMock;
+ private $config;
/**
- * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Payment|MockObject
*/
- private $paymentMock;
+ private $payment;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
private $paymentDO;
/**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
+ * @var OrderAdapterInterface|MockObject
*/
- private $subjectReaderMock;
-
- /**
- * @var OrderAdapterInterface|\PHPUnit_Framework_MockObject_MockObject
- */
- private $orderMock;
+ private $order;
protected function setUp()
{
$this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
- $this->configMock = $this->getMockBuilder(Config::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->paymentMock = $this->getMockBuilder(Payment::class)
+ $this->config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
+ $this->payment = $this->getMockBuilder(Payment::class)
->disableOriginalConstructor()
->getMock();
- $this->orderMock = $this->createMock(OrderAdapterInterface::class);
+ $this->order = $this->createMock(OrderAdapterInterface::class);
- $this->builder = new PaymentDataBuilder($this->configMock, $this->subjectReaderMock);
+ $this->builder = new PaymentDataBuilder($this->config, new SubjectReader());
}
/**
@@ -77,11 +70,6 @@ public function testBuildReadPaymentException()
{
$buildSubject = [];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
-
$this->builder->build($buildSubject);
}
@@ -95,15 +83,6 @@ public function testBuildReadAmountException()
'amount' => null
];
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
- $this->subjectReaderMock->expects(self::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
-
$this->builder->build($buildSubject);
}
@@ -128,36 +107,23 @@ public function testBuild()
'amount' => 10.00
];
- $this->paymentMock->expects(static::exactly(count($additionalData)))
+ $this->payment->expects(self::exactly(count($additionalData)))
->method('getAdditionalInformation')
->willReturnMap($additionalData);
- $this->configMock->expects(static::once())
- ->method('getMerchantAccountId')
+ $this->config->method('getMerchantAccountId')
->willReturn(self::MERCHANT_ACCOUNT_ID);
- $this->paymentDO->expects(static::once())
- ->method('getPayment')
- ->willReturn($this->paymentMock);
-
- $this->paymentDO->expects(static::once())
- ->method('getOrder')
- ->willReturn($this->orderMock);
-
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
- $this->subjectReaderMock->expects(self::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willReturn(10.00);
-
- $this->orderMock->expects(static::once())
- ->method('getOrderIncrementId')
+ $this->paymentDO->method('getPayment')
+ ->willReturn($this->payment);
+
+ $this->paymentDO->method('getOrder')
+ ->willReturn($this->order);
+
+ $this->order->method('getOrderIncrementId')
->willReturn('000000101');
- static::assertEquals(
+ self::assertEquals(
$expectedResult,
$this->builder->build($buildSubject)
);
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php
index 5aa383d095a1e..ff4b2f4545fa6 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/RefundDataBuilderTest.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\PaymentDataBuilder;
use Magento\Braintree\Gateway\Request\RefundDataBuilder;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
@@ -14,11 +14,6 @@
class RefundDataBuilderTest extends \PHPUnit\Framework\TestCase
{
- /**
- * @var SubjectReader | \PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReader;
-
/**
* @var RefundDataBuilder
*/
@@ -26,41 +21,25 @@ class RefundDataBuilderTest extends \PHPUnit\Framework\TestCase
public function setUp()
{
- $this->subjectReader = $this->getMockBuilder(
- SubjectReader::class
- )->disableOriginalConstructor()
- ->getMock();
-
- $this->dataBuilder = new RefundDataBuilder($this->subjectReader);
+ $this->dataBuilder = new RefundDataBuilder(new SubjectReader());
}
public function testBuild()
{
$paymentDO = $this->createMock(PaymentDataObjectInterface::class);
- $paymentModel = $this->getMockBuilder(
- Payment::class
- )->disableOriginalConstructor()
+ $paymentModel = $this->getMockBuilder(Payment::class)
+ ->disableOriginalConstructor()
->getMock();
$buildSubject = ['payment' => $paymentDO, 'amount' => 12.358];
$transactionId = 'xsd7n';
- $this->subjectReader->expects(static::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($paymentDO);
- $paymentDO->expects(static::once())
- ->method('getPayment')
+ $paymentDO->method('getPayment')
->willReturn($paymentModel);
- $paymentModel->expects(static::once())
- ->method('getParentTransactionId')
+ $paymentModel->method('getParentTransactionId')
->willReturn($transactionId);
- $this->subjectReader->expects(static::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willReturn($buildSubject['amount']);
- static::assertEquals(
+ self::assertEquals(
[
'transaction_id' => $transactionId,
PaymentDataBuilder::AMOUNT => '12.36'
@@ -72,30 +51,19 @@ public function testBuild()
public function testBuildNullAmount()
{
$paymentDO = $this->createMock(PaymentDataObjectInterface::class);
- $paymentModel = $this->getMockBuilder(
- Payment::class
- )->disableOriginalConstructor()
+ $paymentModel = $this->getMockBuilder(Payment::class)
+ ->disableOriginalConstructor()
->getMock();
$buildSubject = ['payment' => $paymentDO];
$transactionId = 'xsd7n';
- $this->subjectReader->expects(static::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($paymentDO);
- $paymentDO->expects(static::once())
- ->method('getPayment')
+ $paymentDO->method('getPayment')
->willReturn($paymentModel);
- $paymentModel->expects(static::once())
- ->method('getParentTransactionId')
+ $paymentModel->method('getParentTransactionId')
->willReturn($transactionId);
- $this->subjectReader->expects(static::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
- static::assertEquals(
+ self::assertEquals(
[
'transaction_id' => $transactionId,
PaymentDataBuilder::AMOUNT => null
@@ -107,31 +75,20 @@ public function testBuildNullAmount()
public function testBuildCutOffLegacyTransactionIdPostfix()
{
$paymentDO = $this->createMock(PaymentDataObjectInterface::class);
- $paymentModel = $this->getMockBuilder(
- Payment::class
- )->disableOriginalConstructor()
+ $paymentModel = $this->getMockBuilder(Payment::class)
+ ->disableOriginalConstructor()
->getMock();
$buildSubject = ['payment' => $paymentDO];
$legacyTxnId = 'xsd7n-' . TransactionInterface::TYPE_CAPTURE;
$transactionId = 'xsd7n';
- $this->subjectReader->expects(static::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($paymentDO);
- $paymentDO->expects(static::once())
- ->method('getPayment')
+ $paymentDO->method('getPayment')
->willReturn($paymentModel);
- $paymentModel->expects(static::once())
- ->method('getParentTransactionId')
+ $paymentModel->method('getParentTransactionId')
->willReturn($legacyTxnId);
- $this->subjectReader->expects(static::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willThrowException(new \InvalidArgumentException());
- static::assertEquals(
+ self::assertEquals(
[
'transaction_id' => $transactionId,
PaymentDataBuilder::AMOUNT => null
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/SettlementDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/SettlementDataBuilderTest.php
index 2f8f954243749..beccad6e9a045 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/SettlementDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/SettlementDataBuilderTest.php
@@ -11,7 +11,7 @@ class SettlementDataBuilderTest extends \PHPUnit\Framework\TestCase
{
public function testBuild()
{
- $this->assertEquals(
+ self::assertEquals(
[
'options' => [
SettlementDataBuilder::SUBMIT_FOR_SETTLEMENT => true
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php
index c28ac0c3ac372..e2bcc2c3b3967 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/ThreeDSecureDataBuilderTest.php
@@ -6,68 +6,63 @@
namespace Magento\Braintree\Test\Unit\Gateway\Request;
use Magento\Braintree\Gateway\Config\Config;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder;
-use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
-use Magento\Payment\Gateway\Data\Order\OrderAdapter;
use Magento\Payment\Gateway\Data\Order\AddressAdapter;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Payment\Gateway\Data\Order\OrderAdapter;
+use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class ThreeDSecureDataBuilderTest
*/
class ThreeDSecureDataBuilderTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var int
+ */
+ private static $storeId = 1;
+
/**
* @var ThreeDSecureDataBuilder
*/
private $builder;
/**
- * @var Config|\PHPUnit_Framework_MockObject_MockObject
+ * @var Config|MockObject
*/
- private $configMock;
+ private $config;
/**
- * @var PaymentDataObjectInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentDataObjectInterface|MockObject
*/
private $paymentDO;
/**
- * @var OrderAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var OrderAdapter|MockObject
*/
private $order;
/**
- * @var \Magento\Payment\Gateway\Data\Order\AddressAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var AddressAdapter|MockObject
*/
private $billingAddress;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReaderMock;
-
protected function setUp()
{
$this->initOrderMock();
$this->paymentDO = $this->getMockBuilder(PaymentDataObjectInterface::class)
->disableOriginalConstructor()
- ->setMethods(['getOrder', 'getPayment'])
->getMock();
- $this->paymentDO->expects(static::once())
- ->method('getOrder')
+ $this->paymentDO->method('getOrder')
->willReturn($this->order);
- $this->configMock = $this->getMockBuilder(Config::class)
- ->setMethods(['isVerify3DSecure', 'getThresholdAmount', 'get3DSecureSpecificCountries'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class)
+ $this->config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
- $this->builder = new ThreeDSecureDataBuilder($this->configMock, $this->subjectReaderMock);
+ $this->builder = new ThreeDSecureDataBuilder($this->config, new SubjectReader());
}
/**
@@ -76,7 +71,6 @@ protected function setUp()
* @param string $countryId
* @param array $countries
* @param array $expected
- * @covers \Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder::build
* @dataProvider buildDataProvider
*/
public function testBuild($verify, $thresholdAmount, $countryId, array $countries, array $expected)
@@ -86,37 +80,28 @@ public function testBuild($verify, $thresholdAmount, $countryId, array $countrie
'amount' => 25
];
- $this->configMock->expects(static::once())
- ->method('isVerify3DSecure')
+ $this->config->method('isVerify3DSecure')
+ ->with(self::equalTo(self::$storeId))
->willReturn($verify);
- $this->configMock->expects(static::any())
- ->method('getThresholdAmount')
+ $this->config->method('getThresholdAmount')
+ ->with(self::equalTo(self::$storeId))
->willReturn($thresholdAmount);
- $this->configMock->expects(static::any())
- ->method('get3DSecureSpecificCountries')
+ $this->config->method('get3DSecureSpecificCountries')
+ ->with(self::equalTo(self::$storeId))
->willReturn($countries);
- $this->billingAddress->expects(static::any())
- ->method('getCountryId')
+ $this->billingAddress->method('getCountryId')
->willReturn($countryId);
- $this->subjectReaderMock->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
- $this->subjectReaderMock->expects(self::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willReturn(25);
-
$result = $this->builder->build($buildSubject);
- static::assertEquals($expected, $result);
+ self::assertEquals($expected, $result);
}
/**
- * Get list of variations for build test
+ * Gets list of variations to build request data.
+ *
* @return array
*/
public function buildDataProvider()
@@ -144,22 +129,21 @@ public function buildDataProvider()
}
/**
- * Create mock object for order adapter
+ * Creates mock object for order adapter.
*/
private function initOrderMock()
{
$this->billingAddress = $this->getMockBuilder(AddressAdapter::class)
->disableOriginalConstructor()
- ->setMethods(['getCountryId'])
->getMock();
$this->order = $this->getMockBuilder(OrderAdapter::class)
->disableOriginalConstructor()
- ->setMethods(['getBillingAddress'])
->getMock();
- $this->order->expects(static::any())
- ->method('getBillingAddress')
+ $this->order->method('getBillingAddress')
->willReturn($this->billingAddress);
+ $this->order->method('getStoreId')
+ ->willReturn(self::$storeId);
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php
index df11938ddba70..5af050002eb2d 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php
@@ -5,12 +5,13 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Request;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Api\Data\OrderPaymentExtension;
use Magento\Sales\Model\Order\Payment;
use Magento\Vault\Model\PaymentToken;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase
{
@@ -20,35 +21,25 @@ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase
private $builder;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var PaymentDataObjectInterface|MockObject
*/
private $paymentDO;
/**
- * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Payment|MockObject
*/
private $payment;
- /**
- * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject
- */
- private $subjectReader;
-
public function setUp()
{
$this->paymentDO = $this->createMock(PaymentDataObjectInterface::class);
$this->payment = $this->getMockBuilder(Payment::class)
->disableOriginalConstructor()
->getMock();
- $this->paymentDO->expects(static::once())
- ->method('getPayment')
+ $this->paymentDO->method('getPayment')
->willReturn($this->payment);
- $this->subjectReader = $this->getMockBuilder(SubjectReader::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->builder = new VaultCaptureDataBuilder($this->subjectReader);
+ $this->builder = new VaultCaptureDataBuilder(new SubjectReader());
}
/**
@@ -68,15 +59,6 @@ public function testBuild()
'paymentMethodToken' => $token
];
- $this->subjectReader->expects(self::once())
- ->method('readPayment')
- ->with($buildSubject)
- ->willReturn($this->paymentDO);
- $this->subjectReader->expects(self::once())
- ->method('readAmount')
- ->with($buildSubject)
- ->willReturn($amount);
-
$paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class)
->setMethods(['getVaultPaymentToken'])
->disableOriginalConstructor()
@@ -86,18 +68,15 @@ public function testBuild()
->disableOriginalConstructor()
->getMock();
- $paymentExtension->expects(static::once())
- ->method('getVaultPaymentToken')
+ $paymentExtension->method('getVaultPaymentToken')
->willReturn($paymentToken);
- $this->payment->expects(static::once())
- ->method('getExtensionAttributes')
+ $this->payment->method('getExtensionAttributes')
->willReturn($paymentExtension);
- $paymentToken->expects(static::once())
- ->method('getGatewayToken')
+ $paymentToken->method('getGatewayToken')
->willReturn($token);
$result = $this->builder->build($buildSubject);
- static::assertEquals($expected, $result);
+ self::assertEquals($expected, $result);
}
}
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultDataBuilderTest.php
index 08b5526daeb04..c4bd047a06bb8 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultDataBuilderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultDataBuilderTest.php
@@ -20,7 +20,7 @@ public function testBuild()
$buildSubject = [];
$builder = new VaultDataBuilder();
- static::assertEquals(
+ self::assertEquals(
$expectedResult,
$builder->build($buildSubject)
);
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php
index 87e8e4e413c1b..525b474040ea4 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/CardDetailsHandlerTest.php
@@ -11,7 +11,7 @@
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Sales\Model\Order\Payment;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
/**
* Class CardDetailsHandlerTest
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php
index fdf3dc941bd77..0a3a9f2918a21 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php
@@ -7,7 +7,7 @@
use Braintree\Transaction;
use Braintree\Transaction\PayPalDetails;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Response\PayPal\VaultDetailsHandler;
use Magento\Framework\Intl\DateTimeFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php
index f1420ee895e5b..341135b6c03d0 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPalDetailsHandlerTest.php
@@ -10,7 +10,7 @@
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php
index d90caa84b447b..f941b8d7245d1 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PaymentDetailsHandlerTest.php
@@ -10,7 +10,7 @@
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php
index 2365c396c2f4a..374925b7339c0 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/RiskDataHandlerTest.php
@@ -6,7 +6,7 @@
namespace Magento\Braintree\Test\Unit\Gateway\Response;
use Braintree\Transaction;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Response\RiskDataHandler;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php
index 9ca9ca6aa07ae..6cffc00a89c41 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/ThreeDSecureDetailsHandlerTest.php
@@ -10,7 +10,7 @@
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php
index 3a2d2f7073573..6cbca707242f1 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Response;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Response\TransactionIdHandler;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php
index fb8f507bf1214..568593273c58c 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php
@@ -8,7 +8,7 @@
use Braintree\Transaction;
use Braintree\Transaction\CreditCardDetails;
use Magento\Braintree\Gateway\Config\Config;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Response\VaultDetailsHandler;
use Magento\Framework\DataObject;
use Magento\Payment\Gateway\Data\PaymentDataObject;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php
index 398349a9692b7..a541b0115fe63 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VoidHandlerTest.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Braintree\Test\Unit\Gateway\Response;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use Magento\Braintree\Gateway\Response\VoidHandler;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Sales\Model\Order\Payment;
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php
index 1a9e547e90636..c8a46da504fef 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php
@@ -10,7 +10,7 @@
use Magento\Payment\Gateway\Validator\ResultInterface;
use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
use Magento\Braintree\Gateway\Validator\GeneralResponseValidator;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase
{
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php
index 294226b1656ec..03363b5463d78 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/PaymentNonceResponseValidatorTest.php
@@ -9,7 +9,7 @@
use Magento\Braintree\Gateway\Validator\PaymentNonceResponseValidator;
use Magento\Payment\Gateway\Validator\ResultInterface;
use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
/**
* Class PaymentNonceResponseValidatorTest
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php
index aeb9b4a83077c..4bd446079f9a7 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ResponseValidatorTest.php
@@ -10,7 +10,7 @@
use Magento\Payment\Gateway\Validator\ResultInterface;
use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
use Magento\Braintree\Gateway\Validator\ResponseValidator;
-use Magento\Braintree\Gateway\Helper\SubjectReader;
+use Magento\Braintree\Gateway\SubjectReader;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Braintree\Result\Error;
use Braintree\Result\Successful;
diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php
index e43e67c18744f..5fcb6a89244b5 100644
--- a/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Model/Report/TransactionsCollectionTest.php
@@ -6,10 +6,12 @@
namespace Magento\Braintree\Test\Unit\Model\Report;
use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Braintree\Model\Report\FilterMapper;
use Magento\Braintree\Model\Report\TransactionsCollection;
use Magento\Framework\Api\Search\DocumentInterface;
use Magento\Framework\Data\Collection\EntityFactoryInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class TransactionsCollectionTest
@@ -19,48 +21,58 @@
class TransactionsCollectionTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var BraintreeAdapter|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapter|MockObject
*/
- private $braintreeAdapterMock;
+ private $braintreeAdapter;
/**
- * @var EntityFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var BraintreeAdapterFactory|MockObject
*/
- private $entityFactoryMock;
+ private $adapterFactory;
/**
- * @var FilterMapper|\PHPUnit_Framework_MockObject_MockObject
+ * @var EntityFactoryInterface|MockObject
*/
- private $filterMapperMock;
+ private $entityFactory;
/**
- * @var DocumentInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var FilterMapper|MockObject
*/
- private $transactionMapMock;
+ private $filterMapper;
+
+ /**
+ * @var DocumentInterface|MockObject
+ */
+ private $transactionMap;
/**
* Setup
*/
protected function setUp()
{
- $this->transactionMapMock = $this->getMockBuilder(DocumentInterface::class)
+ $this->transactionMap = $this->getMockBuilder(DocumentInterface::class)
->disableOriginalConstructor()
->getMock();
- $this->entityFactoryMock = $this->getMockBuilder(EntityFactoryInterface::class)
+ $this->entityFactory = $this->getMockBuilder(EntityFactoryInterface::class)
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->filterMapperMock = $this->getMockBuilder(FilterMapper::class)
+ $this->filterMapper = $this->getMockBuilder(FilterMapper::class)
->setMethods(['getFilter'])
->disableOriginalConstructor()
->getMock();
- $this->braintreeAdapterMock = $this->getMockBuilder(BraintreeAdapter::class)
+ $this->braintreeAdapter = $this->getMockBuilder(BraintreeAdapter::class)
->setMethods(['search'])
->disableOriginalConstructor()
->getMock();
+ $this->adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->adapterFactory->method('create')
+ ->willReturn($this->braintreeAdapter);
}
/**
@@ -68,28 +80,26 @@ protected function setUp()
*/
public function testGetItems()
{
- $this->filterMapperMock->expects($this->once())
- ->method('getFilter')
+ $this->filterMapper->method('getFilter')
->willReturn(new BraintreeSearchNodeStub());
- $this->braintreeAdapterMock->expects($this->once())
- ->method('search')
+ $this->braintreeAdapter->method('search')
->willReturn(['transaction1', 'transaction2']);
- $this->entityFactoryMock->expects($this->exactly(2))
+ $this->entityFactory->expects(self::exactly(2))
->method('create')
- ->willReturn($this->transactionMapMock);
+ ->willReturn($this->transactionMap);
$collection = new TransactionsCollection(
- $this->entityFactoryMock,
- $this->braintreeAdapterMock,
- $this->filterMapperMock
+ $this->entityFactory,
+ $this->adapterFactory,
+ $this->filterMapper
);
$collection->addFieldToFilter('orderId', ['like' => '0']);
$items = $collection->getItems();
- $this->assertEquals(2, count($items));
- $this->assertInstanceOf(DocumentInterface::class, $items[1]);
+ self::assertEquals(2, count($items));
+ self::assertInstanceOf(DocumentInterface::class, $items[1]);
}
/**
@@ -97,27 +107,25 @@ public function testGetItems()
*/
public function testGetItemsEmptyCollection()
{
- $this->filterMapperMock->expects($this->once())
- ->method('getFilter')
+ $this->filterMapper->method('getFilter')
->willReturn(new BraintreeSearchNodeStub());
- $this->braintreeAdapterMock->expects($this->once())
- ->method('search')
+ $this->braintreeAdapter->method('search')
->willReturn(null);
- $this->entityFactoryMock->expects($this->never())
+ $this->entityFactory->expects(self::never())
->method('create')
- ->willReturn($this->transactionMapMock);
+ ->willReturn($this->transactionMap);
$collection = new TransactionsCollection(
- $this->entityFactoryMock,
- $this->braintreeAdapterMock,
- $this->filterMapperMock
+ $this->entityFactory,
+ $this->adapterFactory,
+ $this->filterMapper
);
$collection->addFieldToFilter('orderId', ['like' => '0']);
$items = $collection->getItems();
- $this->assertEquals(0, count($items));
+ self::assertEquals(0, count($items));
}
/**
@@ -127,29 +135,27 @@ public function testGetItemsWithLimit()
{
$transations = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10);
- $this->filterMapperMock->expects($this->once())
- ->method('getFilter')
+ $this->filterMapper->method('getFilter')
->willReturn(new BraintreeSearchNodeStub());
- $this->braintreeAdapterMock->expects($this->once())
- ->method('search')
+ $this->braintreeAdapter->method('search')
->willReturn($transations);
- $this->entityFactoryMock->expects($this->exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT))
+ $this->entityFactory->expects(self::exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT))
->method('create')
- ->willReturn($this->transactionMapMock);
+ ->willReturn($this->transactionMap);
$collection = new TransactionsCollection(
- $this->entityFactoryMock,
- $this->braintreeAdapterMock,
- $this->filterMapperMock
+ $this->entityFactory,
+ $this->adapterFactory,
+ $this->filterMapper
);
$collection->setPageSize(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT);
$collection->addFieldToFilter('orderId', ['like' => '0']);
$items = $collection->getItems();
- $this->assertEquals(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT, count($items));
- $this->assertInstanceOf(DocumentInterface::class, $items[1]);
+ self::assertEquals(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT, count($items));
+ self::assertInstanceOf(DocumentInterface::class, $items[1]);
}
/**
@@ -159,29 +165,27 @@ public function testGetItemsWithNullLimit()
{
$transations = range(1, TransactionsCollection::TRANSACTION_MAXIMUM_COUNT + 10);
- $this->filterMapperMock->expects($this->once())
- ->method('getFilter')
+ $this->filterMapper->method('getFilter')
->willReturn(new BraintreeSearchNodeStub());
- $this->braintreeAdapterMock->expects($this->once())
- ->method('search')
+ $this->braintreeAdapter->method('search')
->willReturn($transations);
- $this->entityFactoryMock->expects($this->exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT))
+ $this->entityFactory->expects(self::exactly(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT))
->method('create')
- ->willReturn($this->transactionMapMock);
+ ->willReturn($this->transactionMap);
$collection = new TransactionsCollection(
- $this->entityFactoryMock,
- $this->braintreeAdapterMock,
- $this->filterMapperMock
+ $this->entityFactory,
+ $this->adapterFactory,
+ $this->filterMapper
);
$collection->setPageSize(null);
$collection->addFieldToFilter('orderId', ['like' => '0']);
$items = $collection->getItems();
- $this->assertEquals(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT, count($items));
- $this->assertInstanceOf(DocumentInterface::class, $items[1]);
+ self::assertEquals(TransactionsCollection::TRANSACTION_MAXIMUM_COUNT, count($items));
+ self::assertInstanceOf(DocumentInterface::class, $items[1]);
}
/**
@@ -191,18 +195,18 @@ public function testGetItemsWithNullLimit()
*/
public function testAddToFilter($field, $condition, $filterMapperCall, $expectedCondition)
{
- $this->filterMapperMock->expects(static::exactly($filterMapperCall))
+ $this->filterMapper->expects(self::exactly($filterMapperCall))
->method('getFilter')
->with($field, $expectedCondition)
->willReturn(new BraintreeSearchNodeStub());
$collection = new TransactionsCollection(
- $this->entityFactoryMock,
- $this->braintreeAdapterMock,
- $this->filterMapperMock
+ $this->entityFactory,
+ $this->adapterFactory,
+ $this->filterMapper
);
- static::assertInstanceOf(
+ self::assertInstanceOf(
TransactionsCollection::class,
$collection->addFieldToFilter($field, $condition)
);
diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php
index 6c85ae68eb7af..dca22c26c11da 100644
--- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php
@@ -7,7 +7,9 @@
use Magento\Braintree\Gateway\Config\Config;
use Magento\Braintree\Model\Adapter\BraintreeAdapter;
+use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory;
use Magento\Braintree\Model\Ui\ConfigProvider;
+use Magento\Customer\Model\Session;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
@@ -31,6 +33,11 @@ class ConfigProviderTest extends \PHPUnit\Framework\TestCase
*/
private $braintreeAdapter;
+ /**
+ * @var Session|MockObject
+ */
+ private $session;
+
/**
* @var ConfigProvider
*/
@@ -45,10 +52,24 @@ protected function setUp()
$this->braintreeAdapter = $this->getMockBuilder(BraintreeAdapter::class)
->disableOriginalConstructor()
->getMock();
+ /** @var BraintreeAdapterFactory|MockObject $adapterFactory */
+ $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $adapterFactory->method('create')
+ ->willReturn($this->braintreeAdapter);
+
+ $this->session = $this->getMockBuilder(Session::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStoreId'])
+ ->getMock();
+ $this->session->method('getStoreId')
+ ->willReturn(null);
$this->configProvider = new ConfigProvider(
$this->config,
- $this->braintreeAdapter
+ $adapterFactory,
+ $this->session
);
}
@@ -61,35 +82,30 @@ protected function setUp()
*/
public function testGetConfig($config, $expected)
{
- $this->braintreeAdapter->expects(static::once())
- ->method('generate')
+ $this->braintreeAdapter->method('generate')
->willReturn(self::CLIENT_TOKEN);
foreach ($config as $method => $value) {
- $this->config->expects(static::once())
- ->method($method)
+ $this->config->method($method)
->willReturn($value);
}
- static::assertEquals($expected, $this->configProvider->getConfig());
+ self::assertEquals($expected, $this->configProvider->getConfig());
}
/**
- * @covers \Magento\Braintree\Model\Ui\ConfigProvider::getClientToken
* @dataProvider getClientTokenDataProvider
*/
public function testGetClientToken($merchantAccountId, $params)
{
- $this->config->expects(static::once())
- ->method('getMerchantAccountId')
+ $this->config->method('getMerchantAccountId')
->willReturn($merchantAccountId);
- $this->braintreeAdapter->expects(static::once())
- ->method('generate')
+ $this->braintreeAdapter->method('generate')
->with($params)
->willReturn(self::CLIENT_TOKEN);
- static::assertEquals(self::CLIENT_TOKEN, $this->configProvider->getClientToken());
+ self::assertEquals(self::CLIENT_TOKEN, $this->configProvider->getClientToken());
}
/**
diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json
index 64d2e1950825f..0248a62ccfbb2 100644
--- a/app/code/Magento/Braintree/composer.json
+++ b/app/code/Magento/Braintree/composer.json
@@ -25,7 +25,7 @@
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/Braintree/etc/adminhtml/di.xml b/app/code/Magento/Braintree/etc/adminhtml/di.xml
index d0469ded83b67..90fc927ed4f80 100644
--- a/app/code/Magento/Braintree/etc/adminhtml/di.xml
+++ b/app/code/Magento/Braintree/etc/adminhtml/di.xml
@@ -28,6 +28,7 @@
- Magento\Braintree\Gateway\Request\AddressDataBuilder
- Magento\Braintree\Gateway\Request\VaultDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -39,6 +40,7 @@
- Magento\Braintree\Gateway\Request\ChannelDataBuilder
- Magento\Braintree\Gateway\Request\AddressDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -57,4 +59,9 @@
Magento\Backend\Model\Session\Quote
+
+
+ Magento\Backend\Model\Session\Quote
+
+
diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml
index 2a451e132eab0..86c2911815754 100644
--- a/app/code/Magento/Braintree/etc/di.xml
+++ b/app/code/Magento/Braintree/etc/di.xml
@@ -214,6 +214,7 @@
- Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder
- Magento\Braintree\Gateway\Request\KountPaymentDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -245,6 +246,7 @@
- Magento\Braintree\Gateway\Request\CaptureDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -268,6 +270,7 @@
- Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder
- Magento\Braintree\Gateway\Request\KountPaymentDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -300,6 +303,7 @@
- Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder
- Magento\Braintree\Gateway\Request\SettlementDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -321,6 +325,7 @@
- Magento\Braintree\Gateway\Request\PayPal\VaultDataBuilder
- Magento\Braintree\Gateway\Request\PayPal\DeviceDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -353,6 +358,7 @@
- Magento\Braintree\Gateway\Request\ChannelDataBuilder
- Magento\Braintree\Gateway\Request\AddressDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
@@ -463,23 +469,41 @@
Magento\Braintree\Gateway\Http\Client\TransactionVoid
- Magento\Braintree\Gateway\Request\VoidDataBuilder
+ BraintreeVoidRequestBuilder
Magento\Braintree\Gateway\Response\VoidHandler
Magento\Braintree\Gateway\Validator\GeneralResponseValidator
Magento\Braintree\Gateway\Http\TransferFactory
+
+
+
+ - Magento\Braintree\Gateway\Request\VoidDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
+
+
+
+
Magento\Braintree\Gateway\Http\Client\TransactionRefund
- Magento\Braintree\Gateway\Request\RefundDataBuilder
+ BraintreeRefundBuilder
Magento\Braintree\Gateway\Validator\GeneralResponseValidator
Magento\Braintree\Gateway\Response\RefundHandler
Magento\Braintree\Gateway\Http\TransferFactory
+
+
+
+ - Magento\Braintree\Gateway\Request\RefundDataBuilder
+ - Magento\Braintree\Gateway\Request\StoreConfigBuilder
+
+
+
+
@@ -494,7 +518,7 @@
-
+
diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js
index 8324cfe463bdb..5e1e85e6a3c48 100644
--- a/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js
+++ b/app/code/Magento/Braintree/view/adminhtml/web/js/braintree.js
@@ -116,7 +116,7 @@ define([
},
/**
- * Setup Braintree SDK
+ * Retrieves client token and setup Braintree SDK
*/
initBraintree: function () {
var self = this;
@@ -124,35 +124,14 @@ define([
try {
$('body').trigger('processStart');
- self.braintree.setup(self.clientToken, 'custom', {
- id: self.selector,
- hostedFields: self.getHostedFields(),
-
- /**
- * Triggered when sdk was loaded
- */
- onReady: function () {
- $('body').trigger('processStop');
- },
+ $.getJSON(self.clientTokenUrl).done(function (response) {
+ self.clientToken = response.clientToken;
+ self._initBraintree();
+ }).fail(function (response) {
+ var failed = JSON.parse(response.responseText);
- /**
- * Callback for success response
- * @param {Object} response
- */
- onPaymentMethodReceived: function (response) {
- if (self.validateCardType()) {
- self.setPaymentDetails(response.nonce);
- self.placeOrder();
- }
- },
-
- /**
- * Error callback
- * @param {Object} response
- */
- onError: function (response) {
- self.error(response.message);
- }
+ $('body').trigger('processStop');
+ self.error(failed.message);
});
} catch (e) {
$('body').trigger('processStop');
@@ -160,6 +139,44 @@ define([
}
},
+ /**
+ * Setup Braintree SDK
+ */
+ _initBraintree: function () {
+ var self = this;
+
+ self.braintree.setup(self.clientToken, 'custom', {
+ id: self.selector,
+ hostedFields: self.getHostedFields(),
+
+ /**
+ * Triggered when sdk was loaded
+ */
+ onReady: function () {
+ $('body').trigger('processStop');
+ },
+
+ /**
+ * Callback for success response
+ * @param {Object} response
+ */
+ onPaymentMethodReceived: function (response) {
+ if (self.validateCardType()) {
+ self.setPaymentDetails(response.nonce);
+ self.placeOrder();
+ }
+ },
+
+ /**
+ * Error callback
+ * @param {Object} response
+ */
+ onError: function (response) {
+ self.error(response.message);
+ }
+ });
+ },
+
/**
* Get hosted fields configuration
* @returns {Object}
diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json
index 39b5fe1b1a6d5..0983d4c6b5946 100644
--- a/app/code/Magento/Bundle/composer.json
+++ b/app/code/Magento/Bundle/composer.json
@@ -25,7 +25,7 @@
"magento/module-bundle-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
index f461b52515253..df98969c262ce 100644
--- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
@@ -192,7 +192,14 @@ public function setCollection($collection)
$this->_collection->setPageSize($limit);
}
if ($this->getCurrentOrder()) {
- $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
+ if ($this->getCurrentOrder() == 'position') {
+ $this->_collection->addAttributeToSort(
+ $this->getCurrentOrder(),
+ $this->getCurrentDirection()
+ )->addAttributeToSort('entity_id', $this->getCurrentDirection());
+ } else {
+ $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
+ }
}
return $this;
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
index 8eb13af6f96f2..f2803c2399474 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
@@ -193,7 +193,7 @@ public function execute()
);
}
- $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0, 'apply_to' => []];
+ $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0];
if (is_null($model->getIsUserDefined()) || $model->getIsUserDefined() != 0) {
$data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']);
diff --git a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
index 29a1ef60a43de..f22c6903a230c 100644
--- a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
+++ b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
@@ -119,7 +119,9 @@ private function mergeCategoryLinks($newCategoryPositions, $oldCategoryPositions
if ($key === false) {
$result[] = $newCategoryPosition;
- } elseif ($oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']) {
+ } elseif (isset($oldCategoryPositions[$key])
+ && $oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']
+ ) {
$result[] = $newCategoryPositions[$key];
unset($oldCategoryPositions[$key]);
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
index 24d81f0054c5a..8f0e6e9c0cdb5 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
@@ -369,25 +369,55 @@ protected function getAttributeValues($entityIds, $storeId)
}
$values = [];
- foreach ($entityIds as $entityId) {
- $values[$entityId] = [];
+ $linkIds = $this->getLinkIds($entityIds);
+ foreach ($linkIds as $linkId) {
+ $values[$linkId] = [];
}
+
$attributes = $this->getAttributes();
$attributesType = ['varchar', 'int', 'decimal', 'text', 'datetime'];
+ $linkField = $this->getCategoryMetadata()->getLinkField();
foreach ($attributesType as $type) {
foreach ($this->getAttributeTypeValues($type, $entityIds, $storeId) as $row) {
- if (isset($row[$this->getCategoryMetadata()->getLinkField()]) && isset($row['attribute_id'])) {
+ if (isset($row[$linkField]) && isset($row['attribute_id'])) {
$attributeId = $row['attribute_id'];
if (isset($attributes[$attributeId])) {
$attributeCode = $attributes[$attributeId]['attribute_code'];
- $values[$row[$this->getCategoryMetadata()->getLinkField()]][$attributeCode] = $row['value'];
+ $values[$row[$linkField]][$attributeCode] = $row['value'];
}
}
}
}
+
return $values;
}
+ /**
+ * Translate entity ids into link ids
+ *
+ * Used for rows with no EAV attributes set.
+ *
+ * @param array $entityIds
+ * @return array
+ */
+ private function getLinkIds(array $entityIds)
+ {
+ $linkField = $this->getCategoryMetadata()->getLinkField();
+ if ($linkField === 'entity_id') {
+ return $entityIds;
+ }
+
+ $select = $this->connection->select()->from(
+ ['e' => $this->connection->getTableName($this->getTableName('catalog_category_entity'))],
+ [$linkField]
+ )->where(
+ 'e.entity_id IN (?)',
+ $entityIds
+ );
+
+ return $this->connection->fetchCol($select);
+ }
+
/**
* Return attribute values for given entities and store of specific attribute type
*
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
index eeac2e80af97e..64a8f930d83ee 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
@@ -64,18 +64,22 @@ protected function populateFlatTables(array $stores)
}
/** @TODO Do something with chunks */
$categoriesIdsChunks = array_chunk($categoriesIds[$store->getRootCategoryId()], 500);
+
foreach ($categoriesIdsChunks as $categoriesIdsChunk) {
$attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId());
+ $linkField = $this->categoryMetadata->getLinkField();
+
$data = [];
foreach ($categories[$store->getRootCategoryId()] as $category) {
- if (!isset($attributesData[$category[$this->categoryMetadata->getLinkField()]])) {
+ if (!isset($attributesData[$category[$linkField]])) {
continue;
}
$category['store_id'] = $store->getId();
$data[] = $this->prepareValuesToInsert(
- array_merge($category, $attributesData[$category[$this->categoryMetadata->getLinkField()]])
+ array_merge($category, $attributesData[$category[$linkField]])
);
}
+
$this->connection->insertMultiple(
$this->addTemporaryTableSuffix($this->getMainStoreTable($store->getId())),
$data
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
index 2368c27e02d72..bc17d731f04c0 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
@@ -69,22 +69,24 @@ public function reindex(array $entityIds = [], $useTempTable = false)
$categoriesIdsChunk = $this->filterIdsByStore($categoriesIdsChunk, $store);
$attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId());
+ $linkField = $this->categoryMetadata->getLinkField();
$data = [];
foreach ($categoriesIdsChunk as $categoryId) {
- if (!isset($attributesData[$categoryId])) {
- continue;
- }
-
try {
$category = $this->categoryRepository->get($categoryId);
} catch (NoSuchEntityException $e) {
continue;
}
+ $categoryData = $category->getData();
+ if (!isset($attributesData[$categoryData[$linkField]])) {
+ continue;
+ }
+
$data[] = $this->prepareValuesToInsert(
array_merge(
- $category->getData(),
- $attributesData[$categoryId],
+ $categoryData,
+ $attributesData[$categoryData[$linkField]],
['store_id' => $store->getId()]
)
);
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
index ae0c3554c0d32..e61acc2aa09e5 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
@@ -52,7 +52,6 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
* @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement
* @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
- * @param \Magento\Indexer\Model\Indexer\StateFactory|null $stateFactory
* @param int|null $batchRowsCount
* @param ActiveTableSwitcher|null $activeTableSwitcher
*/
@@ -87,6 +86,20 @@ public function __construct(
$this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class);
}
+ /**
+ * Clear the table we'll be writing de-normalized data into
+ * to prevent archived data getting in the way of actual data.
+ *
+ * @return void
+ */
+ private function clearCurrentTable()
+ {
+ $this->connection->delete(
+ $this->activeTableSwitcher
+ ->getAdditionalTableName($this->getMainTable())
+ );
+ }
+
/**
* Refresh entities index
*
@@ -94,6 +107,7 @@ public function __construct(
*/
public function execute()
{
+ $this->clearCurrentTable();
$this->reindex();
$this->activeTableSwitcher->switchTable($this->connection, [$this->getMainTable()]);
return $this;
@@ -103,6 +117,7 @@ public function execute()
* Return select for remove unnecessary data
*
* @return \Magento\Framework\DB\Select
+ * @deprecated 102.0.1 Not needed anymore.
*/
protected function getSelectUnnecessaryData()
{
@@ -127,12 +142,15 @@ protected function getSelectUnnecessaryData()
* Remove unnecessary data
*
* @return void
+ *
+ * @deprecated 102.0.1 Not needed anymore.
*/
protected function removeUnnecessaryData()
{
- $this->connection->query(
- $this->connection->deleteFromSelect($this->getSelectUnnecessaryData(), $this->getMainTable())
- );
+ //Called for backwards compatibility.
+ $this->getSelectUnnecessaryData();
+ //This method is useless,
+ //left it here just in case somebody's using it in child classes.
}
/**
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
index 05dd94dbe6e57..fbe0d4b550fa6 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
@@ -179,6 +179,11 @@ protected function _createTemporaryFlatTable($storeId)
$columnComment = isset($fieldProp['comment']) ? $fieldProp['comment'] : $fieldName;
+ if ($fieldName == 'created_at') {
+ $columnDefinition['nullable'] = true;
+ $columnDefinition['default'] = null;
+ }
+
$table->addColumn($fieldName, $fieldProp['type'], $columnLength, $columnDefinition, $columnComment);
}
diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php
index cf1392a7e9e8c..cb5669a4bb42e 100644
--- a/app/code/Magento/Catalog/Model/Product.php
+++ b/app/code/Magento/Catalog/Model/Product.php
@@ -1712,7 +1712,7 @@ public function isInStock()
* Get attribute text by its code
*
* @param string $attributeCode Code of the attribute
- * @return string
+ * @return string|array|null
*/
public function getAttributeText($attributeCode)
{
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
index a840498dfbf2b..f2039a5002dcc 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
@@ -40,6 +40,17 @@ public function getItems($attributeCode)
*/
public function add($attributeCode, $option)
{
+ /** @var \Magento\Eav\Api\Data\AttributeOptionInterface[] $currentOptions */
+ $currentOptions = $this->getItems($attributeCode);
+ if (is_array($currentOptions)) {
+ array_walk($currentOptions, function (&$attributeOption) {
+ /** @var \Magento\Eav\Api\Data\AttributeOptionInterface $attributeOption */
+ $attributeOption = $attributeOption->getLabel();
+ });
+ if (in_array($option->getLabel(), $currentOptions)) {
+ return false;
+ }
+ }
return $this->eavOptionManagement->add(
\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE,
$attributeCode,
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
index 977d93f350145..31e322f4e38f2 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
@@ -192,6 +192,7 @@ public function addImage(
$mediaGalleryData['images'][] = [
'file' => $fileName,
'position' => $position,
+ 'media_type' => 'image',
'label' => '',
'disabled' => (int)$exclude,
];
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php
index 0e86510ebcee7..d4c78772e7c0b 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Value.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php
@@ -190,6 +190,7 @@ public function getProduct()
public function saveValues()
{
foreach ($this->getValues() as $value) {
+ $this->isDeleted(false);
$this->setData(
$value
)->setData(
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Config.php b/app/code/Magento/Catalog/Model/ResourceModel/Config.php
index 7fb13265cd130..7b5d4e09a3599 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Config.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Config.php
@@ -149,8 +149,7 @@ public function getAttributesUsedForSortBy()
['main_table' => $this->getTable('eav_attribute')]
)->join(
['additional_table' => $this->getTable('catalog_eav_attribute')],
- 'main_table.attribute_id = additional_table.attribute_id',
- []
+ 'main_table.attribute_id = additional_table.attribute_id'
)->joinLeft(
['al' => $this->getTable('eav_attribute_label')],
'al.attribute_id = main_table.attribute_id AND al.store_id = ' . $this->getStoreId(),
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
index 78db12be56b42..0b85ef38387fa 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
@@ -197,6 +197,21 @@ public function getCategoryDataProvider()
],
[], //affected category_ids
],
+ [
+ [3], //model category_ids
+ [
+ ['category_id' => 3, 'position' => 20],
+ ['category_id' => 4, 'position' => 30],
+ ], // dto category links
+ [
+ ['category_id' => 3, 'position' => 10],
+ ],
+ [
+ ['category_id' => 3, 'position' => 20],
+ ['category_id' => 4, 'position' => 30],
+ ],
+ [3, 4], //affected category_ids
+ ],
];
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php
new file mode 100644
index 0000000000000..abbcef942373e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php
@@ -0,0 +1,107 @@
+resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class);
+ $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
+ $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class);
+
+ $this->model = $objectManager->getObject(
+ \Magento\Catalog\Model\ResourceModel\Config::class,
+ [
+ 'resource' => $this->resource,
+ 'storeManager' => $this->storeManager,
+ 'eavConfig' => $this->eavConfig,
+ ]
+ );
+
+ parent::setUp();
+ }
+
+ public function testGetAttributesUsedForSortBy()
+ {
+ $expression = 'someExpression';
+ $storeId = 1;
+ $entityTypeId = 4;
+
+ $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
+ $selectMock = $this->createMock(\Magento\Framework\DB\Select::class);
+ $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class);
+ $entityTypeMock = $this->createMock(\Magento\Eav\Model\Entity\Type::class);
+
+ $this->resource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock);
+
+ $connectionMock->expects($this->once())->method('getCheckSql')
+ ->with('al.value IS NULL', 'main_table.frontend_label', 'al.value')
+ ->willReturn($expression);
+ $connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock);
+
+ $this->resource->expects($this->exactly(3))->method('getTableName')->withConsecutive(
+ ['eav_attribute'],
+ ['catalog_eav_attribute'],
+ ['eav_attribute_label']
+ )->willReturnOnConsecutiveCalls('eav_attribute', 'catalog_eav_attribute', 'eav_attribute_label');
+
+ $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock);
+ $storeMock->expects($this->once())->method('getId')->willReturn($storeId);
+
+ $this->eavConfig->expects($this->once())->method('getEntityType')->willReturn($entityTypeMock);
+ $entityTypeMock->expects($this->once())->method('getId')->willReturn($entityTypeId);
+
+ $selectMock->expects($this->once())->method('from')
+ ->with(['main_table' => 'eav_attribute'])->willReturn($selectMock);
+ $selectMock->expects($this->once())->method('join')->with(
+ ['additional_table' => 'catalog_eav_attribute'],
+ 'main_table.attribute_id = additional_table.attribute_id'
+ )->willReturn($selectMock);
+ $selectMock->expects($this->once())->method('joinLeft')
+ ->with(
+ ['al' => 'eav_attribute_label'],
+ 'al.attribute_id = main_table.attribute_id AND al.store_id = ' . $storeId,
+ ['store_label' => $expression]
+ )->willReturn($selectMock);
+ $selectMock->expects($this->exactly(2))->method('where')->withConsecutive(
+ ['main_table.entity_type_id = ?', $entityTypeId],
+ ['additional_table.used_for_sort_by = ?', 1]
+ )->willReturn($selectMock);
+
+ $connectionMock->expects($this->once())->method('fetchAll')->with($selectMock);
+
+ $this->model->getAttributesUsedForSortBy();
+ }
+}
diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json
index 5cdf62cdb3405..10b408c545878 100644
--- a/app/code/Magento/Catalog/composer.json
+++ b/app/code/Magento/Catalog/composer.json
@@ -34,7 +34,7 @@
"magento/module-catalog-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "102.0.0",
+ "version": "102.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
index a3cf4b478ad3b..9865589556e7b 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
@@ -59,6 +59,8 @@
}
};
+ var treeRoot = '#tree-div';
+
/**
* Fix ext compatibility with prototype 1.6
*/
@@ -491,7 +493,7 @@
if (data.error) {
reRenderTree();
} else {
- $(obj.tree.container.dom).trigger('categoryMove.tree');
+ $(treeRoot).trigger('categoryMove.tree');
}
$('.page-main-actions').next('.messages').remove();
$('.page-main-actions').next('#messages').remove();
diff --git a/app/code/Magento/CatalogAnalytics/LICENSE.txt b/app/code/Magento/CatalogAnalytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/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/CatalogAnalytics/LICENSE_AFL.txt b/app/code/Magento/CatalogAnalytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/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/CatalogAnalytics/README.md b/app/code/Magento/CatalogAnalytics/README.md
new file mode 100644
index 0000000000000..df125446117a3
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/README.md
@@ -0,0 +1,3 @@
+# Magento_CatalogAnalytics module
+
+The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html).
diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json
new file mode 100644
index 0000000000000..7c622f6fbfa07
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "magento/module-catalog-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "100.2.*",
+ "magento/module-catalog": "101.1.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\CatalogAnalytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogAnalytics/etc/analytics.xml b/app/code/Magento/CatalogAnalytics/etc/analytics.xml
new file mode 100644
index 0000000000000..22d1f2c7d7776
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/etc/analytics.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ products
+
+
+
+
+
diff --git a/app/code/Magento/CatalogAnalytics/etc/module.xml b/app/code/Magento/CatalogAnalytics/etc/module.xml
new file mode 100644
index 0000000000000..7974598e17a59
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogAnalytics/etc/reports.xml b/app/code/Magento/CatalogAnalytics/etc/reports.xml
new file mode 100644
index 0000000000000..5dae3ef90d7b2
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/etc/reports.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogAnalytics/registration.php b/app/code/Magento/CatalogAnalytics/registration.php
new file mode 100644
index 0000000000000..77d6ce154b658
--- /dev/null
+++ b/app/code/Magento/CatalogAnalytics/registration.php
@@ -0,0 +1,11 @@
+_productTypeFactory->create($productTypeConfig['model'], ['params' => $params]))
) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__('Entity type model \'%1\' is not found', $productTypeConfig['model'])
);
}
if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__(
'Entity type model must be an instance of '
. \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class
@@ -1293,20 +1293,15 @@ protected function _saveLinks()
*/
protected function _saveProductAttributes(array $attributesData)
{
+ $linkField = $this->getProductEntityLinkField();
foreach ($attributesData as $tableName => $skuData) {
$tableData = [];
foreach ($skuData as $sku => $attributes) {
- $linkId = $this->_connection->fetchOne(
- $this->_connection->select()
- ->from($this->getResource()->getTable('catalog_product_entity'))
- ->where('sku = ?', (string)$sku)
- ->columns($this->getProductEntityLinkField())
- );
-
+ $linkId = $this->_oldSku[strtolower($sku)][$linkField];
foreach ($attributes as $attributeId => $storeValues) {
foreach ($storeValues as $storeId => $storeValue) {
$tableData[] = [
- $this->getProductEntityLinkField() => $linkId,
+ $linkField => $linkId,
'attribute_id' => $attributeId,
'store_id' => $storeId,
'value' => $storeValue,
@@ -1316,6 +1311,7 @@ protected function _saveProductAttributes(array $attributesData)
}
$this->_connection->insertOnDuplicate($tableName, $tableData, ['value']);
}
+
return $this;
}
@@ -1556,6 +1552,7 @@ public function getImagesFromRow(array $rowData)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ * @throws LocalizedException
*/
protected function _saveProducts()
{
@@ -1616,7 +1613,7 @@ protected function _saveProducts()
// wrong attribute_set_code was received
if (!$attributeSetId) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__(
'Wrong attribute set code "%1", please correct it and try again.',
$rowData['attribute_set_code']
@@ -2003,7 +2000,7 @@ protected function _getUploader()
}
if (!$this->_fileUploader->setTmpDir($tmpPath)) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__('File directory \'%1\' is not readable.', $tmpPath)
);
}
@@ -2012,7 +2009,7 @@ protected function _getUploader()
$this->_mediaDirectory->create($destinationPath);
if (!$this->_fileUploader->setDestDir($destinationPath)) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__('File directory \'%1\' is not writable.', $destinationPath)
);
}
@@ -2034,6 +2031,7 @@ public function getUploader()
* Return a new file name if the same file is already exists.
*
* @param string $fileName
+ * @param bool $renameFileOff
* @return string
*/
protected function uploadMediaFiles($fileName, $renameFileOff = false)
@@ -2758,9 +2756,7 @@ private function _customFieldsMapping($rowData)
}
/**
- * Validate data rows and save bunches to DB
- *
- * @return $this
+ * {@inheritdoc}
*/
protected function _saveValidatedBunches()
{
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
index 5681b1aa6607d..939d6b2de67ee 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
@@ -534,7 +534,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe
public function clearEmptyData(array $rowData)
{
foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) {
- if (!$attrParams['is_static'] && empty($rowData[$attrCode])) {
+ if (!$attrParams['is_static'] && !isset($rowData[$attrCode])) {
unset($rowData[$attrCode]);
}
}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
index 8461e3830cbec..6d183fc8e6e20 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
@@ -6,7 +6,6 @@
namespace Magento\CatalogImportExport\Test\Unit\Model\Import;
use Magento\Framework\App\Filesystem\DirectoryList;
-use Magento\Framework\Stdlib\DateTime;
use Magento\ImportExport\Model\Import;
/**
@@ -513,25 +512,6 @@ public function testSaveProductAttributes()
]
]
];
- $entityTable = 'catalog_product_entity';
- $resource = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel::class)
- ->disableOriginalConstructor()
- ->setMethods(['getTable'])
- ->getMock();
- $resource->expects($this->once())->method('getTable')->with($entityTable)->willReturnArgument(0);
- $this->_resourceFactory->expects($this->once())->method('create')->willReturn($resource);
- $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
- ->disableOriginalConstructor()
- ->getMock();
- $selectMock->expects($this->once())->method('from')->with($entityTable, '*', null)->willReturnSelf();
- $selectMock->expects($this->once())->method('where')->with('sku = ?', $testSku)->willReturnSelf();
- $selectMock->expects($this->once())->method('columns')->with('entity_id')->willReturnSelf();
- $this->_connection->expects($this->any())->method('fetchOne')->willReturn(self::ENTITY_ID);
- $this->_connection->expects($this->any())->method('select')->willReturn($selectMock);
- $this->_connection->expects($this->any())
- ->method('quoteInto')
- ->willReturnCallback([$this, 'returnQuoteCallback']);
-
$tableData[] = [
'entity_id' => self::ENTITY_ID,
'attribute_id' => $attributeId,
@@ -541,6 +521,7 @@ public function testSaveProductAttributes()
$this->_connection->expects($this->once())
->method('insertOnDuplicate')
->with($testTable, $tableData, ['value']);
+ $this->setPropertyValue($this->importProduct, '_oldSku', [$testSku => ['entity_id' => self::ENTITY_ID]]);
$object = $this->invokeMethod($this->importProduct, '_saveProductAttributes', [$attributesData]);
$this->assertEquals($this->importProduct, $object);
}
diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json
index 5ef316b2c065c..2b70f65d1f830 100644
--- a/app/code/Magento/CatalogImportExport/composer.json
+++ b/app/code/Magento/CatalogImportExport/composer.json
@@ -16,7 +16,7 @@
"ext-ctype": "*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php
new file mode 100644
index 0000000000000..8fa90cf6531c4
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php
@@ -0,0 +1,77 @@
+criteriaInterfaceFactory = $criteriaInterfaceFactory;
+ $this->stockItemRepository = $stockItemRepository;
+ $this->stockConfiguration = $stockConfiguration;
+ }
+
+ /**
+ * Add stock items to products in collection.
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var Collection $productCollection */
+ $productCollection = $observer->getData('collection');
+ $productIds = array_keys($productCollection->getItems());
+ $criteria = $this->criteriaInterfaceFactory->create();
+ $criteria->setProductsFilter($productIds);
+ $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());
+ $stockItemCollection = $this->stockItemRepository->getList($criteria);
+ foreach ($stockItemCollection->getItems() as $item) {
+ /** @var Product $product */
+ $product = $productCollection->getItemById($item->getProductId());
+ $productExtension = $product->getExtensionAttributes();
+ $productExtension->setStockItem($item);
+ $product->setExtensionAttributes($productExtension);
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php
new file mode 100644
index 0000000000000..8de05bd014039
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php
@@ -0,0 +1,165 @@
+criteriaInterfaceFactoryMock = $this->getMockBuilder(StockItemCriteriaInterfaceFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class)
+ ->setMethods(['getList'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class)
+ ->setMethods(['getDefaultScopeId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->subject = $objectManager->getObject(
+ AddStockItemsObserver::class,
+ [
+ 'criteriaInterfaceFactory' => $this->criteriaInterfaceFactoryMock,
+ 'stockItemRepository' => $this->stockItemRepositoryMock,
+ 'stockConfiguration' => $this->stockConfigurationMock
+ ]
+ );
+ }
+
+ /**
+ * Test AddStockItemsObserver::execute() add stock item to product as extension attribute.
+ */
+ public function testExecute()
+ {
+ $productId = 1;
+ $defaultScopeId = 0;
+
+ $criteria = $this->getMockBuilder(StockItemCriteriaInterface::class)
+ ->setMethods(['setProductsFilter', 'setScopeFilter'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $criteria->expects(self::once())
+ ->method('setProductsFilter')
+ ->with(self::identicalTo([$productId]))
+ ->willReturn(true);
+ $criteria->expects(self::once())
+ ->method('setScopeFilter')
+ ->with(self::identicalTo($defaultScopeId))
+ ->willReturn(true);
+
+ $this->criteriaInterfaceFactoryMock->expects(self::once())
+ ->method('create')
+ ->willReturn($criteria);
+ $stockItemCollection = $this->getMockBuilder(StockItemCollectionInterface::class)
+ ->setMethods(['getItems'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $stockItem = $this->getMockBuilder(StockItemInterface::class)
+ ->setMethods(['getProductId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $stockItem->expects(self::once())
+ ->method('getProductId')
+ ->willReturn($productId);
+
+ $stockItemCollection->expects(self::once())
+ ->method('getItems')
+ ->willReturn([$stockItem]);
+
+ $this->stockItemRepositoryMock->expects(self::once())
+ ->method('getList')
+ ->with(self::identicalTo($criteria))
+ ->willReturn($stockItemCollection);
+
+ $this->stockConfigurationMock->expects(self::once())
+ ->method('getDefaultScopeId')
+ ->willReturn($defaultScopeId);
+
+ $productExtension = $this->getMockBuilder(ProductExtensionInterface::class)
+ ->setMethods(['setStockItem'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $productExtension->expects(self::once())
+ ->method('setStockItem')
+ ->with(self::identicalTo($stockItem));
+
+ $product = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $product->expects(self::once())
+ ->method('getExtensionAttributes')
+ ->willReturn($productExtension);
+ $product->expects(self::once())
+ ->method('setExtensionAttributes')
+ ->with(self::identicalTo($productExtension))
+ ->willReturnSelf();
+
+ /** @var ProductCollection|\PHPUnit_Framework_MockObject_MockObject $productCollection */
+ $productCollection = $this->getMockBuilder(ProductCollection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $productCollection->expects(self::once())
+ ->method('getItems')
+ ->willReturn([$productId => $product]);
+ $productCollection->expects(self::once())
+ ->method('getItemById')
+ ->with(self::identicalTo($productId))
+ ->willReturn($product);
+
+ /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observer */
+ $observer = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $observer->expects(self::once())
+ ->method('getData')
+ ->with('collection')
+ ->willReturn($productCollection);
+
+ $this->subject->execute($observer);
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json
index 142ae98422e87..79f86612d41bd 100644
--- a/app/code/Magento/CatalogInventory/composer.json
+++ b/app/code/Magento/CatalogInventory/composer.json
@@ -13,7 +13,7 @@
"magento/module-ui": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CatalogInventory/etc/events.xml b/app/code/Magento/CatalogInventory/etc/events.xml
index 3b5f2483ec57e..0a9f3c2d40dca 100644
--- a/app/code/Magento/CatalogInventory/etc/events.xml
+++ b/app/code/Magento/CatalogInventory/etc/events.xml
@@ -42,4 +42,7 @@
+
+
+
diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json
index 8bee82f6fe34b..845bdeff31f42 100644
--- a/app/code/Magento/CatalogRule/composer.json
+++ b/app/code/Magento/CatalogRule/composer.json
@@ -17,7 +17,7 @@
"magento/module-catalog-rule-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "101.0.0",
+ "version": "101.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json
index 8cdf797e1ca4c..13be21e3248f2 100644
--- a/app/code/Magento/CatalogSearch/composer.json
+++ b/app/code/Magento/CatalogSearch/composer.json
@@ -15,7 +15,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php
index 748589924d916..f0351467e5f0e 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DbStorage.php
@@ -12,25 +12,37 @@
class DbStorage extends BaseDbStorage
{
/**
- * @param array $data
- * @return \Magento\Framework\DB\Select
+ * {@inheritDoc}
*/
protected function prepareSelect(array $data)
{
+ $metadata = [];
+ if (array_key_exists(UrlRewrite::METADATA, $data)) {
+ $metadata = $data[UrlRewrite::METADATA];
+ unset($data[UrlRewrite::METADATA]);
+ }
+
$select = $this->connection->select();
- $select->from(['url_rewrite' => $this->resource->getTableName('url_rewrite')])
- ->joinLeft(
- ['relation' => $this->resource->getTableName(Product::TABLE_NAME)],
- 'url_rewrite.url_rewrite_id = relation.url_rewrite_id'
- )
- ->where('url_rewrite.entity_id IN (?)', $data['entity_id'])
- ->where('url_rewrite.entity_type = ?', $data['entity_type'])
- ->where('url_rewrite.store_id IN (?)', $data['store_id']);
- if (empty($data[UrlRewrite::METADATA]['category_id'])) {
+ $select->from([
+ 'url_rewrite' => $this->resource->getTableName(self::TABLE_NAME)
+ ]);
+ $select->joinLeft(
+ ['relation' => $this->resource->getTableName(Product::TABLE_NAME)],
+ 'url_rewrite.url_rewrite_id = relation.url_rewrite_id'
+ );
+
+ foreach ($data as $column => $value) {
+ $select->where('url_rewrite.' . $column . ' IN (?)', $value);
+ }
+ if (empty($metadata['category_id'])) {
$select->where('relation.category_id IS NULL');
} else {
- $select->where('relation.category_id = ?', $data[UrlRewrite::METADATA]['category_id']);
+ $select->where(
+ 'relation.category_id = ?',
+ $metadata['category_id']
+ );
}
+
return $select;
}
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php
new file mode 100644
index 0000000000000..d00b0c87fa5ad
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php
@@ -0,0 +1,142 @@
+urlRewriteFactory = $this
+ ->getMockBuilder(UrlRewriteFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->dataObjectHelper = $this->createMock(DataObjectHelper::class);
+ $this->connectionMock = $this->createMock(AdapterInterface::class);
+ $this->select = $this->createPartialMock(
+ Select::class,
+ ['from', 'where', 'deleteFromSelect', 'joinLeft']
+ );
+ $this->resource = $this->createMock(ResourceConnection::class);
+
+ $this->resource->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue($this->connectionMock));
+ $this->connectionMock->expects($this->any())
+ ->method('select')
+ ->will($this->returnValue($this->select));
+
+ $this->storage = (new ObjectManager($this))->getObject(
+ DbStorage::class,
+ [
+ 'urlRewriteFactory' => $this->urlRewriteFactory,
+ 'dataObjectHelper' => $this->dataObjectHelper,
+ 'resource' => $this->resource,
+ ]
+ );
+ }
+
+ public function testPrepareSelect()
+ {
+ //Passing expected parameters, checking select built.
+ $entityType = 'custom';
+ $entityId= 42;
+ $storeId = 0;
+ $categoryId = 2;
+ $redirectType = 301;
+ //Expecting this methods to be called on select
+ $this->select
+ ->expects($this->at(2))
+ ->method('where')
+ ->with('url_rewrite.entity_id IN (?)', $entityId)
+ ->willReturn($this->select);
+ $this->select
+ ->expects($this->at(3))
+ ->method('where')
+ ->with('url_rewrite.entity_type IN (?)', $entityType)
+ ->willReturn($this->select);
+ $this->select
+ ->expects($this->at(4))
+ ->method('where')
+ ->with('url_rewrite.store_id IN (?)', $storeId)
+ ->willReturn($this->select);
+ $this->select
+ ->expects($this->at(5))
+ ->method('where')
+ ->with('url_rewrite.redirect_type IN (?)', $redirectType)
+ ->willReturn($this->select);
+ $this->select
+ ->expects($this->at(6))
+ ->method('where')
+ ->with('relation.category_id = ?', $categoryId)
+ ->willReturn($this->select);
+ //Preparing mocks to be used
+ $this->select
+ ->expects($this->any())
+ ->method('from')
+ ->willReturn($this->select);
+ $this->select
+ ->expects($this->any())
+ ->method('joinLeft')
+ ->willReturn($this->select);
+ //Indirectly calling prepareSelect
+ $this->storage->findOneByData([
+ UrlRewrite::ENTITY_ID => $entityId,
+ UrlRewrite::ENTITY_TYPE => $entityType,
+ UrlRewrite::STORE_ID => $storeId,
+ UrlRewrite::REDIRECT_TYPE => $redirectType,
+ UrlRewrite::METADATA => ['category_id' => $categoryId]
+ ]);
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json
index f11467e9f8d1f..344562864384f 100644
--- a/app/code/Magento/CatalogUrlRewrite/composer.json
+++ b/app/code/Magento/CatalogUrlRewrite/composer.json
@@ -14,7 +14,7 @@
"magento/module-ui": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json
index fa4b5071d4dd6..48d6562c5c77e 100644
--- a/app/code/Magento/Checkout/composer.json
+++ b/app/code/Magento/Checkout/composer.json
@@ -27,7 +27,7 @@
"magento/module-cookie": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv
index 53e6ef67d68cd..8d297c4060abd 100644
--- a/app/code/Magento/Checkout/i18n/en_US.csv
+++ b/app/code/Magento/Checkout/i18n/en_US.csv
@@ -62,7 +62,7 @@ Checkout,Checkout
Error!,Error!
"DB exception","DB exception"
Message,Message
-"Print receipt ","Print receipt "
+"Print receipt","Print receipt"
"Apply Discount Code","Apply Discount Code"
"Enter discount code","Enter discount code"
"Apply Discount","Apply Discount"
diff --git a/app/code/Magento/Checkout/view/frontend/templates/button.phtml b/app/code/Magento/Checkout/view/frontend/templates/button.phtml
index e2bcd76cba35b..c3edfe30f8bdd 100644
--- a/app/code/Magento/Checkout/view/frontend/templates/button.phtml
+++ b/app/code/Magento/Checkout/view/frontend/templates/button.phtml
@@ -10,6 +10,8 @@
getCanViewOrder() && $block->getCanPrintOrder()) :?>
- = /* @escapeNotVerified */ __('Print receipt ', $block->getPrintUrl()) ?>
+
+ = /* @escapeNotVerified */ __('Print receipt') ?>
+
= $block->getChildHtml() ?>
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js
index 1063284ecf0e2..bfcd0d02585bb 100644
--- a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js
+++ b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js
@@ -47,7 +47,7 @@ define([
steps.sort(this.sortItems).forEach(function (element) {
if (element.code == hashString || element.alias == hashString) { //eslint-disable-line eqeqeq
- element.navigate();
+ element.navigate(element);
} else {
element.isVisible(false);
}
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
index 8a8c32042c5e8..619de95e467f0 100644
--- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
@@ -127,10 +127,12 @@ define([
},
/**
- * Load data from server for shipping step
+ * Navigator change hash handler.
+ *
+ * @param {Object} step - navigation step
*/
- navigate: function () {
- //load data from server for shipping step
+ navigate: function (step) {
+ step && step.isVisible(true);
},
/**
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
index 7fd1ee6020937..57f92a713ecb0 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php
@@ -6,6 +6,15 @@
*/
namespace Magento\Cms\Controller\Adminhtml\Page;
+use Magento\Cms\Model\Page\DomValidationState;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Config\Dom\ValidationException;
+use Magento\Framework\Config\Dom\ValidationSchemaException;
+
+/**
+ * Class PostDataProcessor
+ * @package Magento\Cms\Controller\Adminhtml\Page
+ */
class PostDataProcessor
{
/**
@@ -23,19 +32,28 @@ class PostDataProcessor
*/
protected $messageManager;
+ /**
+ * @var DomValidationState
+ */
+ private $validationState;
+
/**
* @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter
* @param \Magento\Framework\Message\ManagerInterface $messageManager
* @param \Magento\Framework\View\Model\Layout\Update\ValidatorFactory $validatorFactory
+ * @param DomValidationState $validationState
*/
public function __construct(
\Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter,
\Magento\Framework\Message\ManagerInterface $messageManager,
- \Magento\Framework\View\Model\Layout\Update\ValidatorFactory $validatorFactory
+ \Magento\Framework\View\Model\Layout\Update\ValidatorFactory $validatorFactory,
+ DomValidationState $validationState = null
) {
$this->dateFilter = $dateFilter;
$this->messageManager = $messageManager;
$this->validatorFactory = $validatorFactory;
+ $this->validationState = $validationState
+ ?: ObjectManager::getInstance()->get(DomValidationState::class);
}
/**
@@ -61,27 +79,27 @@ public function filter($data)
* Validate post data
*
* @param array $data
- * @return bool Return FALSE if someone item is invalid
+ * @return bool Return FALSE if some item is invalid
*/
public function validate($data)
{
- $errorNo = true;
if (!empty($data['layout_update_xml']) || !empty($data['custom_layout_update_xml'])) {
- /** @var $validatorCustomLayout \Magento\Framework\View\Model\Layout\Update\Validator */
- $validatorCustomLayout = $this->validatorFactory->create();
- if (!empty($data['layout_update_xml']) && !$validatorCustomLayout->isValid($data['layout_update_xml'])) {
- $errorNo = false;
- }
- if (!empty($data['custom_layout_update_xml'])
- && !$validatorCustomLayout->isValid($data['custom_layout_update_xml'])
- ) {
- $errorNo = false;
- }
- foreach ($validatorCustomLayout->getMessages() as $message) {
- $this->messageManager->addError($message);
+ /** @var $layoutXmlValidator \Magento\Framework\View\Model\Layout\Update\Validator */
+ $layoutXmlValidator = $this->validatorFactory->create(
+ [
+ 'validationState' => $this->validationState,
+ ]
+ );
+
+ if (!$this->validateData($data, $layoutXmlValidator)) {
+ $validatorMessages = $layoutXmlValidator->getMessages();
+ foreach ($validatorMessages as $message) {
+ $this->messageManager->addErrorMessage($message);
+ }
+ return false;
}
}
- return $errorNo;
+ return true;
}
/**
@@ -108,4 +126,34 @@ public function validateRequireEntry(array $data)
}
return $errorNo;
}
+
+ /**
+ * Validate data, avoid cyclomatic complexity
+ *
+ * @param array $data
+ * @param \Magento\Framework\View\Model\Layout\Update\Validator $layoutXmlValidator
+ * @return bool
+ */
+ private function validateData($data, $layoutXmlValidator)
+ {
+ try {
+ if (!empty($data['layout_update_xml']) && !$layoutXmlValidator->isValid($data['layout_update_xml'])) {
+ return false;
+ }
+ if (!empty($data['custom_layout_update_xml']) &&
+ !$layoutXmlValidator->isValid($data['custom_layout_update_xml'])
+ ) {
+ return false;
+ }
+ } catch (ValidationException $e) {
+ return false;
+ } catch (ValidationSchemaException $e) {
+ return false;
+ } catch (\Exception $e) {
+ $this->messageManager->addExceptionMessage($e);
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/app/code/Magento/Cms/Model/Page/DomValidationState.php b/app/code/Magento/Cms/Model/Page/DomValidationState.php
new file mode 100644
index 0000000000000..b08ab6342fc5d
--- /dev/null
+++ b/app/code/Magento/Cms/Model/Page/DomValidationState.php
@@ -0,0 +1,26 @@
+getConfigPath() !== null ? $field->getConfigPath() : $path;
$data = $this->getConfigValue($path);
+ if ($field->hasBackendModel()) {
+ $backendModel = $field->getBackendModel();
+ // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface
+ if (!$backendModel instanceof ProcessorInterface) {
+ $backendModel->setPath($path)
+ ->setValue($data)
+ ->setWebsite($this->getWebsiteCode())
+ ->setStore($this->getStoreCode())
+ ->afterLoad();
+ $data = $backendModel->getValue();
+ }
+ }
}
return $data;
diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php
index 634f470e87567..3d5713357c39c 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php
@@ -5,7 +5,6 @@
*/
namespace Magento\Config\Model\Config\Backend;
-use Magento\Framework\App\Config\Data\ProcessorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Serialize\Serializer\Json;
@@ -13,7 +12,7 @@
* @api
* @since 100.0.2
*/
-class Serialized extends \Magento\Framework\App\Config\Value implements ProcessorInterface
+class Serialized extends \Magento\Framework\App\Config\Value
{
/**
* @var Json
@@ -68,12 +67,4 @@ public function beforeSave()
parent::beforeSave();
return $this;
}
-
- /**
- * @inheritdoc
- */
- public function processValue($value)
- {
- return empty($value) ? '' : $this->serializer->unserialize($value);
- }
}
diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
index e4170fbe8732e..72e8386bddaf6 100644
--- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
@@ -548,7 +548,7 @@ public function initFieldsDataProvider()
false,
'some_value',
null,
- 0,
+ 1,
false,
false,
false
@@ -560,7 +560,7 @@ public function initFieldsDataProvider()
true,
'Config Value',
null,
- 0,
+ 1,
true,
false,
true
diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json
index fb4c80115334d..a29f65473ab01 100644
--- a/app/code/Magento/Config/composer.json
+++ b/app/code/Magento/Config/composer.json
@@ -13,7 +13,7 @@
"magento/module-deploy": "100.2.*"
},
"type": "magento2-module",
- "version": "101.0.0",
+ "version": "101.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
index 360a11cf2748f..e6345af40f37a 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
@@ -1296,7 +1296,7 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach
if ($salableOnly) {
$collection = $this->salableProcessor->process($collection);
}
- $usedProducts = $collection->getItems();
+ $usedProducts = array_values($collection->getItems());
$this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
}
$product->setData($dataFieldName, $usedProducts);
diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json
index a62ce29813ce3..3b60f57c2f923 100644
--- a/app/code/Magento/ConfigurableProduct/composer.json
+++ b/app/code/Magento/ConfigurableProduct/composer.json
@@ -24,7 +24,7 @@
"magento/module-product-links-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Contact/Controller/Index/Post.php b/app/code/Magento/Contact/Controller/Index/Post.php
index 3374ff1fa5cf4..ee2d23b74df24 100644
--- a/app/code/Magento/Contact/Controller/Index/Post.php
+++ b/app/code/Magento/Contact/Controller/Index/Post.php
@@ -9,12 +9,13 @@
use Magento\Contact\Model\ConfigInterface;
use Magento\Contact\Model\MailInterface;
use Magento\Framework\App\Action\Context;
-use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Request\DataPersistorInterface;
use Magento\Framework\Controller\Result\Redirect;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\HTTP\PhpEnvironment\Request;
use Psr\Log\LoggerInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\DataObject;
class Post extends \Magento\Contact\Controller\Index
{
@@ -56,7 +57,7 @@ public function __construct(
$this->context = $context;
$this->mail = $mail;
$this->dataPersistor = $dataPersistor;
- $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LoggerInterface::class);
+ $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class);
}
/**
@@ -71,45 +72,33 @@ public function execute()
}
try {
$this->sendEmail($this->validatedParams());
- $this->messageManager->addSuccess(
+ $this->messageManager->addSuccessMessage(
__('Thanks for contacting us with your comments and questions. We\'ll respond to you very soon.')
);
- $this->getDataPersistor()->clear('contact_us');
+ $this->dataPersistor->clear('contact_us');
} catch (LocalizedException $e) {
$this->messageManager->addErrorMessage($e->getMessage());
- $this->getDataPersistor()->set('contact_us', $this->getRequest()->getParams());
+ $this->dataPersistor->set('contact_us', $this->getRequest()->getParams());
} catch (\Exception $e) {
$this->logger->critical($e);
$this->messageManager->addErrorMessage(
__('An error occurred while processing your form. Please try again later.')
);
- $this->getDataPersistor()->set('contact_us', $this->getRequest()->getParams());
+ $this->dataPersistor->set('contact_us', $this->getRequest()->getParams());
}
return $this->resultRedirectFactory->create()->setPath('contact/index');
}
- /**
- * Get Data Persistor
- *
- * @return DataPersistorInterface
- */
- private function getDataPersistor()
- {
- if ($this->dataPersistor === null) {
- $this->dataPersistor = ObjectManager::getInstance()
- ->get(DataPersistorInterface::class);
- }
-
- return $this->dataPersistor;
- }
-
/**
* @param array $post Post data from contact form
* @return void
*/
private function sendEmail($post)
{
- $this->mail->send($post['email'], ['data' => new \Magento\Framework\DataObject($post)]);
+ $this->mail->send(
+ $post['email'],
+ ['data' => new DataObject($post)]
+ );
}
/**
diff --git a/app/code/Magento/Contact/Model/Mail.php b/app/code/Magento/Contact/Model/Mail.php
index d63efbbca573b..43c1974252b5a 100644
--- a/app/code/Magento/Contact/Model/Mail.php
+++ b/app/code/Magento/Contact/Model/Mail.php
@@ -6,9 +6,10 @@
namespace Magento\Contact\Model;
use Magento\Framework\Mail\Template\TransportBuilder;
-use Magento\Framework\App\ObjectManager;
use Magento\Framework\Translate\Inline\StateInterface;
use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\Area;
class Mail implements MailInterface
{
@@ -49,8 +50,7 @@ public function __construct(
$this->contactsConfig = $contactsConfig;
$this->transportBuilder = $transportBuilder;
$this->inlineTranslation = $inlineTranslation;
- $this->storeManager = $storeManager ?:
- ObjectManager::getInstance()->get(StoreManagerInterface::class);
+ $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
}
/**
@@ -71,7 +71,7 @@ public function send($replyTo, array $variables)
->setTemplateIdentifier($this->contactsConfig->emailTemplate())
->setTemplateOptions(
[
- 'area' => 'frontend',
+ 'area' => Area::AREA_FRONTEND,
'store' => $this->storeManager->getStore()->getId()
]
)
diff --git a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php
index a18540d8cf9d5..2133ae5a323b4 100644
--- a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php
+++ b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php
@@ -38,7 +38,7 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa
public function get($email, $websiteId = null);
/**
- * Get customer by customer ID.
+ * Get customer by Customer ID.
*
* @param int $customerId
* @return \Magento\Customer\Api\Data\CustomerInterface
@@ -70,7 +70,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr
public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer);
/**
- * Delete customer by ID.
+ * Delete customer by Customer ID.
*
* @param int $customerId
* @return bool true on success
diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php
index 6362f28a4f96d..e9e894b2bc130 100644
--- a/app/code/Magento/Customer/Block/Address/Edit.php
+++ b/app/code/Magento/Customer/Block/Address/Edit.php
@@ -5,7 +5,9 @@
*/
namespace Magento\Customer\Block\Address;
+use Magento\Customer\Model\AttributeChecker;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\App\ObjectManager;
/**
* Customer address edit block
@@ -46,6 +48,11 @@ class Edit extends \Magento\Directory\Block\Data
*/
protected $dataObjectHelper;
+ /**
+ * @var AttributeChecker
+ */
+ private $attributeChecker;
+
/**
* Constructor
*
@@ -61,7 +68,7 @@ class Edit extends \Magento\Directory\Block\Data
* @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer
* @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
* @param array $data
- *
+ * @param AttributeChecker $attributeChecker
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -76,13 +83,16 @@ public function __construct(
\Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory,
\Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer,
\Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
- array $data = []
+ array $data = [],
+ AttributeChecker $attributeChecker = null
) {
$this->_customerSession = $customerSession;
$this->_addressRepository = $addressRepository;
$this->addressDataFactory = $addressDataFactory;
$this->currentCustomer = $currentCustomer;
$this->dataObjectHelper = $dataObjectHelper;
+ $this->attributeChecker = $attributeChecker ?: ObjectManager::getInstance()->get(AttributeChecker::class);
+
parent::__construct(
$context,
$directoryHelper,
@@ -352,4 +362,16 @@ public function getConfig($path)
{
return $this->_scopeConfig->getValue($path, \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
}
+
+ /**
+ * Checks whether it is allowed to show an attribute on the form.
+ *
+ * @param string $attributeCode
+ * @param string $formName
+ * @return bool
+ */
+ public function isAttributeAllowedOnForm($attributeCode, $formName)
+ {
+ return $this->attributeChecker->isAttributeAllowedOnForm($attributeCode, $formName);
+ }
}
diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php
index f1384ba188a0a..8664e0cbf87ea 100644
--- a/app/code/Magento/Customer/Controller/Ajax/Login.php
+++ b/app/code/Magento/Customer/Controller/Ajax/Login.php
@@ -13,6 +13,8 @@
use Magento\Customer\Model\Account\Redirect as AccountRedirect;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Stdlib\CookieManagerInterface;
+use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory;
/**
* Login controller
@@ -58,6 +60,16 @@ class Login extends \Magento\Framework\App\Action\Action
*/
protected $scopeConfig;
+ /**
+ * @var CookieManagerInterface
+ */
+ private $cookieManager;
+
+ /**
+ * @var CookieMetadataFactory
+ */
+ private $cookieMetadataFactory;
+
/**
* Initialize Login controller
*
@@ -67,6 +79,8 @@ class Login extends \Magento\Framework\App\Action\Action
* @param AccountManagementInterface $customerAccountManagement
* @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
* @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory
+ * @param CookieManagerInterface $cookieManager
+ * @param CookieMetadataFactory $cookieMetadataFactory
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
@@ -74,7 +88,9 @@ public function __construct(
\Magento\Framework\Json\Helper\Data $helper,
AccountManagementInterface $customerAccountManagement,
\Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
- \Magento\Framework\Controller\Result\RawFactory $resultRawFactory
+ \Magento\Framework\Controller\Result\RawFactory $resultRawFactory,
+ CookieManagerInterface $cookieManager = null,
+ CookieMetadataFactory $cookieMetadataFactory = null
) {
parent::__construct($context);
$this->customerSession = $customerSession;
@@ -82,6 +98,12 @@ public function __construct(
$this->customerAccountManagement = $customerAccountManagement;
$this->resultJsonFactory = $resultJsonFactory;
$this->resultRawFactory = $resultRawFactory;
+ $this->cookieManager = $cookieManager ?: ObjectManager::getInstance()->get(
+ CookieManagerInterface::class
+ );
+ $this->cookieMetadataFactory = $cookieMetadataFactory ?: ObjectManager::getInstance()->get(
+ CookieMetadataFactory::class
+ );
}
/**
@@ -169,6 +191,11 @@ public function execute()
$this->customerSession->setCustomerDataAsLoggedIn($customer);
$this->customerSession->regenerateId();
$redirectRoute = $this->getAccountRedirect()->getRedirectCookie();
+ if ($this->cookieManager->getCookie('mage-cache-sessid')) {
+ $metadata = $this->cookieMetadataFactory->createCookieMetadata();
+ $metadata->setPath('/');
+ $this->cookieManager->deleteCookie('mage-cache-sessid', $metadata);
+ }
if (!$this->getScopeConfig()->getValue('customer/startup/redirect_dashboard') && $redirectRoute) {
$response['redirectUrl'] = $this->_redirect->success($redirectRoute);
$this->getAccountRedirect()->clearRedirectCookie();
diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php
index 71775ff8f8ce1..6e73e070c790d 100644
--- a/app/code/Magento/Customer/Controller/Section/Load.php
+++ b/app/code/Magento/Customer/Controller/Section/Load.php
@@ -64,6 +64,8 @@ public function execute()
{
/** @var \Magento\Framework\Controller\Result\Json $resultJson */
$resultJson = $this->resultJsonFactory->create();
+ $resultJson->setHeader('Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store');
+ $resultJson->setHeader('Pragma', 'no-cache');
try {
$sectionNames = $this->getRequest()->getParam('sections');
$sectionNames = $sectionNames ? array_unique(\explode(',', $sectionNames)) : null;
diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php
index 8dac704aaa085..c74c62dc6d98c 100644
--- a/app/code/Magento/Customer/Helper/Address.php
+++ b/app/code/Magento/Customer/Helper/Address.php
@@ -8,9 +8,7 @@
use Magento\Customer\Api\AddressMetadataInterface;
use Magento\Customer\Api\CustomerMetadataInterface;
use Magento\Customer\Api\Data\AttributeMetadataInterface;
-use Magento\Customer\Model\Metadata\AttributeResolver;
use Magento\Directory\Model\Country\Format;
-use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\NoSuchEntityException;
/**
@@ -95,11 +93,6 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper
*/
protected $_addressConfig;
- /**
- * @var AttributeResolver
- */
- private $attributeResolver;
-
/**
* @param \Magento\Framework\App\Helper\Context $context
* @param \Magento\Framework\View\Element\BlockFactory $blockFactory
@@ -107,7 +100,6 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper
* @param CustomerMetadataInterface $customerMetadataService
* @param AddressMetadataInterface $addressMetadataService
* @param \Magento\Customer\Model\Address\Config $addressConfig
- * @param AttributeResolver|null $attributeResolver
*/
public function __construct(
\Magento\Framework\App\Helper\Context $context,
@@ -115,15 +107,13 @@ public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
CustomerMetadataInterface $customerMetadataService,
AddressMetadataInterface $addressMetadataService,
- \Magento\Customer\Model\Address\Config $addressConfig,
- AttributeResolver $attributeResolver = null
+ \Magento\Customer\Model\Address\Config $addressConfig
) {
$this->_blockFactory = $blockFactory;
$this->_storeManager = $storeManager;
$this->_customerMetadataService = $customerMetadataService;
$this->_addressMetadataService = $addressMetadataService;
$this->_addressConfig = $addressConfig;
- $this->attributeResolver = $attributeResolver ?: ObjectManager::getInstance()->get(AttributeResolver::class);
parent::__construct($context);
}
@@ -401,31 +391,4 @@ public function isAttributeVisible($code)
}
return false;
}
-
- /**
- * Checks whether it is allowed to show an attribute on the form
- *
- * This check relies on the attribute's property 'getUsedInForms' which contains a list of forms
- * where allowed to render specified attribute.
- *
- * @param string $attributeCode
- * @param string $formName
- * @return bool
- */
- public function isAttributeAllowedOnForm($attributeCode, $formName)
- {
- $isAllowed = false;
- $attributeMetadata = $this->_addressMetadataService->getAttributeMetadata($attributeCode);
- if ($attributeMetadata) {
- /** @var \Magento\Customer\Model\Attribute $attribute */
- $attribute = $this->attributeResolver->getModelByAttribute(
- \Magento\Customer\Api\AddressMetadataManagementInterface::ENTITY_TYPE_ADDRESS,
- $attributeMetadata
- );
- $usedInForms = $attribute->getUsedInForms();
- $isAllowed = in_array($formName, $usedInForms, true);
- }
-
- return $isAllowed;
- }
}
diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php
index 59205a4d91838..ba646549f6919 100644
--- a/app/code/Magento/Customer/Model/AccountManagement.php
+++ b/app/code/Magento/Customer/Model/AccountManagement.php
@@ -531,12 +531,8 @@ public function initiatePasswordReset($email, $template, $websiteId = null)
$this->getEmailNotification()->passwordResetConfirmation($customer);
break;
default:
- throw new InputException(
- __(
- 'Invalid value of "%value" provided for the %fieldName field.',
- ['value' => $template, 'fieldName' => 'email type']
- )
- );
+ $this->handleUnknownTemplate($template);
+ break;
}
return true;
} catch (MailException $e) {
@@ -546,6 +542,25 @@ public function initiatePasswordReset($email, $template, $websiteId = null)
return false;
}
+ /**
+ * Handle not supported template
+ *
+ * @param string $template
+ * @throws InputException
+ */
+ private function handleUnknownTemplate($template)
+ {
+ throw new InputException(__(
+ 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.',
+ [
+ 'value' => $template,
+ 'fieldName' => 'template',
+ 'template1' => AccountManagement::EMAIL_REMINDER,
+ 'template2' => AccountManagement::EMAIL_RESET
+ ]
+ ));
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php
index 3b141d4cb7f68..a6ba510932d3d 100644
--- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php
+++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php
@@ -269,7 +269,7 @@ public function setData($key, $value = null)
{
if (is_array($key)) {
$key = $this->_implodeArrayField($key);
- } elseif (is_array($value) && !empty($value) && $this->isAddressMultilineAttribute($key)) {
+ } elseif (is_array($value) && $this->isAddressMultilineAttribute($key)) {
$value = $this->_implodeArrayValues($value);
}
return parent::setData($key, $value);
@@ -309,7 +309,11 @@ protected function _implodeArrayField(array $data)
*/
protected function _implodeArrayValues($value)
{
- if (is_array($value) && count($value)) {
+ if (is_array($value)) {
+ if (!count($value)) {
+ return '';
+ }
+
$isScalar = false;
foreach ($value as $val) {
if (is_scalar($val)) {
diff --git a/app/code/Magento/Customer/Model/AttributeChecker.php b/app/code/Magento/Customer/Model/AttributeChecker.php
new file mode 100644
index 0000000000000..6cc27697ccff7
--- /dev/null
+++ b/app/code/Magento/Customer/Model/AttributeChecker.php
@@ -0,0 +1,66 @@
+addressMetadata = $addressMetadata;
+ $this->attributeResolver = $attributeResolver;
+ }
+
+ /**
+ * Checks whether it is allowed to show an attribute on the form
+ *
+ * This check relies on the attribute's property 'getUsedInForms' which contains a list of forms
+ * where allowed to render specified attribute.
+ *
+ * @param string $attributeCode
+ * @param string $formName
+ * @return bool
+ */
+ public function isAttributeAllowedOnForm($attributeCode, $formName)
+ {
+ $isAllowed = false;
+ $attributeMetadata = $this->addressMetadata->getAttributeMetadata($attributeCode);
+ if ($attributeMetadata) {
+ /** @var Attribute $attribute */
+ $attribute = $this->attributeResolver->getModelByAttribute(
+ AddressMetadataManagementInterface::ENTITY_TYPE_ADDRESS,
+ $attributeMetadata
+ );
+ $usedInForms = $attribute->getUsedInForms();
+ $isAllowed = in_array($formName, $usedInForms, true);
+ }
+
+ return $isAllowed;
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php
index b759b1a62573f..2fca6c99be319 100644
--- a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php
@@ -73,6 +73,21 @@ class LoginTest extends \PHPUnit\Framework\TestCase
*/
protected $redirectMock;
+ /**
+ * @var \Magento\Framework\Stdlib\CookieManagerInterface| \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cookieManager;
+
+ /**
+ * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory| \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cookieMetadataFactory;
+
+ /**
+ * @var \Magento\Framework\Stdlib\Cookie\CookieMetadata| \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cookieMetadata;
+
protected function setUp()
{
$this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class)
@@ -100,6 +115,16 @@ protected function setUp()
->setMethods(['create'])
->getMock();
+ $this->cookieManager = $this->getMockBuilder(\Magento\Framework\Stdlib\CookieManagerInterface::class)
+ ->setMethods(['getCookie', 'deleteCookie'])
+ ->getMockForAbstractClass();
+ $this->cookieMetadataFactory = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->cookieMetadata = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieMetadata::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
$this->resultRaw = $this->getMockBuilder(\Magento\Framework\Controller\Result\Raw::class)
->disableOriginalConstructor()
->getMock();
@@ -128,6 +153,8 @@ protected function setUp()
'resultJsonFactory' => $this->resultJsonFactory,
'objectManager' => $this->objectManager,
'customerAccountManagement' => $this->customerAccountManagementMock,
+ 'cookieManager' => $this->cookieManager,
+ 'cookieMetadataFactory' => $this->cookieMetadataFactory
]
);
}
@@ -179,6 +206,22 @@ public function testLogin()
$this->object->setAccountRedirect($redirectMock);
$redirectMock->expects($this->once())->method('getRedirectCookie')->willReturn('some_url1');
+ $this->cookieManager->expects($this->once())
+ ->method('getCookie')
+ ->with('mage-cache-sessid')
+ ->willReturn(true);
+ $this->cookieMetadataFactory->expects($this->once())
+ ->method('createCookieMetadata')
+ ->willReturn($this->cookieMetadata);
+ $this->cookieMetadata->expects($this->once())
+ ->method('setPath')
+ ->with('/')
+ ->willReturnSelf();
+ $this->cookieManager->expects($this->once())
+ ->method('deleteCookie')
+ ->with('mage-cache-sessid', $this->cookieMetadata)
+ ->willReturnSelf();
+
$scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$this->object->setScopeConfig($scopeConfigMock);
$scopeConfigMock->expects($this->once())->method('getValue')
diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php
new file mode 100644
index 0000000000000..2552beeca463d
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php
@@ -0,0 +1,181 @@
+contextMock = $this->createMock(Context::class);
+ $this->resultJsonFactoryMock = $this->createMock(JsonFactory::class);
+ $this->sectionIdentifierMock = $this->createMock(Identifier::class);
+ $this->sectionPoolMock = $this->getMockForAbstractClass(SectionPoolInterface::class);
+ $this->escaperMock = $this->createMock(Escaper::class);
+ $this->httpRequestMock = $this->createMock(HttpRequest::class);
+ $this->resultJsonMock = $this->createMock(Json::class);
+
+ $this->contextMock->expects($this->once())
+ ->method('getRequest')
+ ->willReturn($this->httpRequestMock);
+
+ $this->loadAction = new Load(
+ $this->contextMock,
+ $this->resultJsonFactoryMock,
+ $this->sectionIdentifierMock,
+ $this->sectionPoolMock,
+ $this->escaperMock
+ );
+ }
+
+ /**
+ * @param $sectionNames
+ * @param $updateSectionID
+ * @param $sectionNamesAsArray
+ * @param $updateIds
+ * @dataProvider executeDataProvider
+ */
+ public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArray, $updateIds)
+ {
+ $this->resultJsonFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->resultJsonMock);
+ $this->resultJsonMock->expects($this->exactly(2))
+ ->method('setHeader')
+ ->withConsecutive(
+ ['Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store'],
+ ['Pragma', 'no-cache']
+ );
+
+ $this->httpRequestMock->expects($this->exactly(2))
+ ->method('getParam')
+ ->withConsecutive(['sections'], ['update_section_id'])
+ ->willReturnOnConsecutiveCalls($sectionNames, $updateSectionID);
+
+ $this->sectionPoolMock->expects($this->once())
+ ->method('getSectionsData')
+ ->with($sectionNamesAsArray, $updateIds)
+ ->willReturn([
+ 'message' => 'some message',
+ 'someKey' => 'someValue'
+ ]);
+
+ $this->resultJsonMock->expects($this->once())
+ ->method('setData')
+ ->with([
+ 'message' => 'some message',
+ 'someKey' => 'someValue'
+ ])
+ ->willReturn($this->resultJsonMock);
+
+ $this->loadAction->execute();
+ }
+
+ public function executeDataProvider()
+ {
+ return [
+ [
+ 'sectionNames' => 'sectionName1,sectionName2,sectionName3',
+ 'updateSectionID' => 'updateSectionID',
+ 'sectionNamesAsArray' => ['sectionName1', 'sectionName2', 'sectionName3'],
+ 'updateIds' => true
+ ],
+ [
+ 'sectionNames' => null,
+ 'updateSectionID' => null,
+ 'sectionNamesAsArray' => null,
+ 'updateIds' => false
+ ],
+ ];
+ }
+
+ public function testExecuteWithThrowException()
+ {
+ $this->resultJsonFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->resultJsonMock);
+ $this->resultJsonMock->expects($this->exactly(2))
+ ->method('setHeader')
+ ->withConsecutive(
+ ['Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store'],
+ ['Pragma', 'no-cache']
+ );
+
+ $this->httpRequestMock->expects($this->once())
+ ->method('getParam')
+ ->with('sections')
+ ->willThrowException(new \Exception('Some Message'));
+
+ $this->resultJsonMock->expects($this->once())
+ ->method('setStatusHeader')
+ ->with(
+ \Zend\Http\Response::STATUS_CODE_400,
+ \Zend\Http\AbstractMessage::VERSION_11,
+ 'Bad Request'
+ );
+
+ $this->escaperMock->expects($this->once())
+ ->method('escapeHtml')
+ ->with('Some Message')
+ ->willReturn('Some Message');
+
+ $this->resultJsonMock->expects($this->once())
+ ->method('setData')
+ ->with(['message' => 'Some Message'])
+ ->willReturn($this->resultJsonMock);
+
+ $this->loadAction->execute();
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php
index 7b42e8b7774c8..50785247d7965 100644
--- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php
@@ -7,7 +7,6 @@
namespace Magento\Customer\Test\Unit\Helper;
use Magento\Customer\Api\AddressMetadataInterface;
-use Magento\Customer\Api\AddressMetadataManagementInterface;
use Magento\Customer\Api\CustomerMetadataInterface;
/**
@@ -36,9 +35,6 @@ class AddressTest extends \PHPUnit\Framework\TestCase
/** @var \Magento\Customer\Model\Address\Config|\PHPUnit_Framework_MockObject_MockObject */
protected $addressConfig;
- /** @var \Magento\Customer\Model\Metadata\AttributeResolver|\PHPUnit_Framework_MockObject_MockObject */
- protected $attributeResolver;
-
/** @var \PHPUnit_Framework_MockObject_MockObject|AddressMetadataInterface */
private $addressMetadataService;
@@ -55,7 +51,6 @@ protected function setUp()
$this->customerMetadataService = $arguments['customerMetadataService'];
$this->addressConfig = $arguments['addressConfig'];
$this->addressMetadataService = $arguments['addressMetadataService'];
- $this->attributeResolver = $arguments['attributeResolver'];
$this->helper = $objectManagerHelper->getObject($className, $arguments);
}
@@ -408,68 +403,4 @@ public function isAttributeVisibleDataProvider()
['invalid_code', false],
];
}
-
- /**
- * @dataProvider attributeOnFormDataProvider
- * @param bool $isAllowed
- * @param bool $isMetadataExists
- * @param string $attributeCode
- * @param string $formName
- * @param array $attributeFormsList
- */
- public function testIsAttributeAllowedOnForm(
- $isAllowed,
- $isMetadataExists,
- $attributeCode,
- $formName,
- array $attributeFormsList
- ) {
- $attributeMetadata = null;
- if ($isMetadataExists) {
- $attributeMetadata = $this->getMockBuilder(\Magento\Customer\Api\Data\AttributeMetadataInterface::class)
- ->getMockForAbstractClass();
- $attribute = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->attributeResolver->expects($this->once())
- ->method('getModelByAttribute')
- ->with(AddressMetadataManagementInterface::ENTITY_TYPE_ADDRESS, $attributeMetadata)
- ->willReturn($attribute);
- $attribute->expects($this->once())
- ->method('getUsedInForms')
- ->willReturn($attributeFormsList);
- }
- $this->addressMetadataService->expects($this->once())
- ->method('getAttributeMetadata')
- ->with($attributeCode)
- ->willReturn($attributeMetadata);
- $this->assertEquals($isAllowed, $this->helper->isAttributeAllowedOnForm($attributeCode, $formName));
- }
-
- public function attributeOnFormDataProvider()
- {
- return [
- 'metadata not exists' => [
- 'isAllowed' => false,
- 'isMetadataExists' => false,
- 'attributeCode' => 'attribute_code',
- 'formName' => 'form_name',
- 'attributeFormsList' => [],
- ],
- 'form not in the list' => [
- 'isAllowed' => false,
- 'isMetadataExists' => true,
- 'attributeCode' => 'attribute_code',
- 'formName' => 'form_name',
- 'attributeFormsList' => ['form_1', 'form_2'],
- ],
- 'allowed' => [
- 'isAllowed' => true,
- 'isMetadataExists' => true,
- 'attributeCode' => 'attribute_code',
- 'formName' => 'form_name',
- 'attributeFormsList' => ['form_name', 'form_1', 'form_2'],
- ],
- ];
- }
}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
index 34662eecc0c1c..2a6b9fe6fd4ea 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
@@ -1173,10 +1173,6 @@ public function testInitiatePasswordResetEmailReset()
$this->assertTrue($this->accountManagement->initiatePasswordReset($email, $template));
}
- /**
- * @expectedException \Magento\Framework\Exception\InputException
- * @expectedExceptionMessage Invalid value of "" provided for the email type field
- */
public function testInitiatePasswordResetNoTemplate()
{
$storeId = 1;
@@ -1192,6 +1188,10 @@ public function testInitiatePasswordResetNoTemplate()
$this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash);
+ $this->expectException(\Magento\Framework\Exception\InputException::class);
+ $this->expectExceptionMessage(
+ 'Invalid value of "" provided for the template field. Possible values: email_reminder or email_reset.'
+ );
$this->accountManagement->initiatePasswordReset($email, $template);
}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
index 23b8b38c962b9..2eef9a44cab74 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
@@ -366,6 +366,15 @@ public function testGetStreetFullAlwaysReturnsString($expectedResult, $street)
$this->assertEquals($expectedResult, $this->model->getStreetFull());
}
+ /**
+ * @dataProvider getStreetFullDataProvider
+ */
+ public function testSetDataStreetAlwaysConvertedToString($expectedResult, $street)
+ {
+ $this->model->setData('street', $street);
+ $this->assertEquals($expectedResult, $this->model->getData('street'));
+ }
+
/**
* @return array
*/
diff --git a/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php b/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php
new file mode 100644
index 0000000000000..480f5be96e318
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php
@@ -0,0 +1,104 @@
+addressMetadataService = $this->getMockForAbstractClass(AddressMetadataInterface::class);
+ $this->attributeResolver = $this->getMockBuilder(AttributeResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = new AttributeChecker(
+ $this->addressMetadataService,
+ $this->attributeResolver
+ );
+ }
+
+ /**
+ * @param bool $isAllowed
+ * @param bool $isMetadataExists
+ * @param string $attributeCode
+ * @param string $formName
+ * @param array $attributeFormsList
+ *
+ * @dataProvider attributeOnFormDataProvider
+ */
+ public function testIsAttributeAllowedOnForm(
+ $isAllowed,
+ $isMetadataExists,
+ $attributeCode,
+ $formName,
+ array $attributeFormsList
+ ) {
+ $attributeMetadata = null;
+ if ($isMetadataExists) {
+ $attributeMetadata = $this->getMockForAbstractClass(AttributeMetadataInterface::class);
+ $attribute = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->attributeResolver->expects($this->once())
+ ->method('getModelByAttribute')
+ ->with(AddressMetadataManagementInterface::ENTITY_TYPE_ADDRESS, $attributeMetadata)
+ ->willReturn($attribute);
+ $attribute->expects($this->once())
+ ->method('getUsedInForms')
+ ->willReturn($attributeFormsList);
+ }
+ $this->addressMetadataService->expects($this->once())
+ ->method('getAttributeMetadata')
+ ->with($attributeCode)
+ ->willReturn($attributeMetadata);
+
+ $this->assertEquals($isAllowed, $this->model->isAttributeAllowedOnForm($attributeCode, $formName));
+ }
+
+ public function attributeOnFormDataProvider()
+ {
+ return [
+ 'metadata not exists' => [
+ 'isAllowed' => false,
+ 'isMetadataExists' => false,
+ 'attributeCode' => 'attribute_code',
+ 'formName' => 'form_name',
+ 'attributeFormsList' => [],
+ ],
+ 'form not in the list' => [
+ 'isAllowed' => false,
+ 'isMetadataExists' => true,
+ 'attributeCode' => 'attribute_code',
+ 'formName' => 'form_name',
+ 'attributeFormsList' => ['form_1', 'form_2'],
+ ],
+ 'allowed' => [
+ 'isAllowed' => true,
+ 'isMetadataExists' => true,
+ 'attributeCode' => 'attribute_code',
+ 'formName' => 'form_name',
+ 'attributeFormsList' => ['form_name', 'form_1', 'form_2'],
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json
index 0e8ebf957edf5..61fcb62932992 100644
--- a/app/code/Magento/Customer/composer.json
+++ b/app/code/Magento/Customer/composer.json
@@ -29,7 +29,7 @@
"magento/module-customer-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "101.0.0",
+ "version": "101.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml
index c35e9fa4b4c8a..da4b80536e631 100644
--- a/app/code/Magento/Customer/etc/config.xml
+++ b/app/code/Magento/Customer/etc/config.xml
@@ -56,7 +56,7 @@
- 1
+ 0
{{depend prefix}}{{var prefix}} {{/depend}}{{var firstname}} {{depend middlename}}{{var middlename}} {{/depend}}{{var lastname}}{{depend suffix}} {{var suffix}}{{/depend}}
diff --git a/app/code/Magento/Customer/etc/webapi.xml b/app/code/Magento/Customer/etc/webapi.xml
index 26a9c41f20c0d..c536e26bcc82a 100644
--- a/app/code/Magento/Customer/etc/webapi.xml
+++ b/app/code/Magento/Customer/etc/webapi.xml
@@ -128,7 +128,7 @@
-
+
diff --git a/app/code/Magento/CustomerAnalytics/LICENSE.txt b/app/code/Magento/CustomerAnalytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/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/CustomerAnalytics/LICENSE_AFL.txt b/app/code/Magento/CustomerAnalytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/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/CustomerAnalytics/README.md b/app/code/Magento/CustomerAnalytics/README.md
new file mode 100644
index 0000000000000..8c64ce97629da
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/README.md
@@ -0,0 +1,3 @@
+# Magento_CustomerAnalytics module
+
+The Magento_CustomerAnalytics module configures data definitions for a data collection related to the Customer module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html).
diff --git a/app/code/Magento/CustomerAnalytics/composer.json b/app/code/Magento/CustomerAnalytics/composer.json
new file mode 100644
index 0000000000000..d34d6ba751e2a
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "magento/module-customer-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "100.2.*",
+ "magento/module-customer": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\CustomerAnalytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerAnalytics/etc/analytics.xml b/app/code/Magento/CustomerAnalytics/etc/analytics.xml
new file mode 100644
index 0000000000000..5e47040c2f3bd
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/etc/analytics.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ customers
+
+
+
+
+
diff --git a/app/code/Magento/CustomerAnalytics/etc/module.xml b/app/code/Magento/CustomerAnalytics/etc/module.xml
new file mode 100644
index 0000000000000..adc4f8dd849c2
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CustomerAnalytics/etc/reports.xml b/app/code/Magento/CustomerAnalytics/etc/reports.xml
new file mode 100644
index 0000000000000..b3300b0127709
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/etc/reports.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CustomerAnalytics/registration.php b/app/code/Magento/CustomerAnalytics/registration.php
new file mode 100644
index 0000000000000..e4c3348182877
--- /dev/null
+++ b/app/code/Magento/CustomerAnalytics/registration.php
@@ -0,0 +1,11 @@
+_connection->insertOnDuplicate(
$this->_entityTable,
$entitiesToUpdate,
- $this->customerFields
+ $this->getCustomerEntityFieldsToUpdate($entitiesToUpdate)
);
}
return $this;
}
+ /**
+ * Filter the entity that are being updated so we only change fields found in the importer file
+ *
+ * @param array $entitiesToUpdate
+ * @return array
+ */
+ private function getCustomerEntityFieldsToUpdate(array $entitiesToUpdate): array
+ {
+ $firstCustomer = reset($entitiesToUpdate);
+ $columnsToUpdate = array_keys($firstCustomer);
+ $customerFieldsToUpdate = array_filter($this->customerFields, function ($field) use ($columnsToUpdate) {
+ return in_array($field, $columnsToUpdate);
+ });
+ return $customerFieldsToUpdate;
+ }
+
/**
* Save customer attributes.
*
diff --git a/app/code/Magento/Deploy/Model/Filesystem.php b/app/code/Magento/Deploy/Model/Filesystem.php
index 3dd28f4d3e820..0557914f48d24 100644
--- a/app/code/Magento/Deploy/Model/Filesystem.php
+++ b/app/code/Magento/Deploy/Model/Filesystem.php
@@ -5,17 +5,16 @@
*/
namespace Magento\Deploy\Model;
-use Symfony\Component\Console\Output\OutputInterface;
-use Magento\Framework\App\State;
-use Magento\Framework\App\DeploymentConfig\Writer;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Validator\Locale;
use Magento\User\Model\ResourceModel\User\Collection as UserCollection;
+use Symfony\Component\Console\Output\OutputInterface;
/**
* Generate static files, compile
*
- * Сlear generated/code, generated/metadata/, var/view_preprocessed and pub/static directories
+ * Clear generated/code, generated/metadata/, var/view_preprocessed and pub/static directories
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -50,21 +49,6 @@ class Filesystem
*/
const DEFAULT_THEME = 'Magento/blank';
- /**
- * @var \Magento\Framework\App\DeploymentConfig\Writer
- */
- private $writer;
-
- /**
- * @var \Magento\Framework\App\DeploymentConfig\Reader
- */
- private $reader;
-
- /**
- * @var \Magento\Framework\ObjectManagerInterface
- */
- private $objectManager;
-
/**
* @var \Magento\Framework\Filesystem
*/
@@ -101,33 +85,35 @@ class Filesystem
private $userCollection;
/**
- * @param \Magento\Framework\App\DeploymentConfig\Writer $writer
- * @param \Magento\Framework\App\DeploymentConfig\Reader $reader
- * @param \Magento\Framework\ObjectManagerInterface $objectManager
+ * @var Locale
+ */
+ private $locale;
+
+ /**
* @param \Magento\Framework\Filesystem $filesystem
* @param \Magento\Framework\App\Filesystem\DirectoryList $directoryList
* @param \Magento\Framework\Filesystem\Driver\File $driverFile
* @param \Magento\Store\Model\Config\StoreView $storeView
* @param \Magento\Framework\ShellInterface $shell
+ * @param UserCollection $userCollection
+ * @param Locale $locale
*/
public function __construct(
- \Magento\Framework\App\DeploymentConfig\Writer $writer,
- \Magento\Framework\App\DeploymentConfig\Reader $reader,
- \Magento\Framework\ObjectManagerInterface $objectManager,
\Magento\Framework\Filesystem $filesystem,
\Magento\Framework\App\Filesystem\DirectoryList $directoryList,
\Magento\Framework\Filesystem\Driver\File $driverFile,
\Magento\Store\Model\Config\StoreView $storeView,
- \Magento\Framework\ShellInterface $shell
+ \Magento\Framework\ShellInterface $shell,
+ UserCollection $userCollection,
+ Locale $locale
) {
- $this->writer = $writer;
- $this->reader = $reader;
- $this->objectManager = $objectManager;
$this->filesystem = $filesystem;
$this->directoryList = $directoryList;
$this->driverFile = $driverFile;
$this->storeView = $storeView;
$this->shell = $shell;
+ $this->userCollection = $userCollection;
+ $this->locale = $locale;
$this->functionCallPath =
PHP_BINARY . ' -f ' . BP . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'magento ';
}
@@ -141,7 +127,7 @@ public function __construct(
public function regenerateStatic(
OutputInterface $output
) {
- // Сlear generated/code, generated/metadata/, var/view_preprocessed and pub/static directories
+ // Clear generated/code, generated/metadata/, var/view_preprocessed and pub/static directories
$this->cleanupFilesystem(
[
DirectoryList::CACHE,
@@ -193,7 +179,7 @@ protected function deployStaticContent(
private function getAdminUserInterfaceLocales()
{
$locales = [];
- foreach ($this->getUserCollection() as $user) {
+ foreach ($this->userCollection as $user) {
$locales[] = $user->getInterfaceLocale();
}
return $locales;
@@ -203,6 +189,7 @@ private function getAdminUserInterfaceLocales()
* Get used store and admin user locales
*
* @return array
+ * @throws \InvalidArgumentException if unknown locale is provided by the store configuration
*/
private function getUsedLocales()
{
@@ -210,25 +197,18 @@ private function getUsedLocales()
$this->storeView->retrieveLocales(),
$this->getAdminUserInterfaceLocales()
);
- return array_unique($usedLocales);
- }
-
- /**
- * Get user collection
- *
- * @return UserCollection
- * @deprecated 100.1.0 Added to not break backward compatibility of the constructor signature
- * by injecting the new dependency directly.
- * The method can be removed in a future major release, when constructor signature can be changed.
- */
- private function getUserCollection()
- {
- if (!($this->userCollection instanceof UserCollection)) {
- return \Magento\Framework\App\ObjectManager::getInstance()->get(
- UserCollection::class
- );
- }
- return $this->userCollection;
+ return array_map(
+ function ($locale) {
+ if (!$this->locale->isValid($locale)) {
+ throw new \InvalidArgumentException(
+ $locale .
+ ' argument has invalid value, run info:language:list for list of available locales'
+ );
+ }
+ return $locale;
+ },
+ array_unique($usedLocales)
+ );
}
/**
diff --git a/app/code/Magento/Deploy/Package/Bundle/RequireJs.php b/app/code/Magento/Deploy/Package/Bundle/RequireJs.php
index 73a15554f8b78..c7c9e5315c7ab 100644
--- a/app/code/Magento/Deploy/Package/Bundle/RequireJs.php
+++ b/app/code/Magento/Deploy/Package/Bundle/RequireJs.php
@@ -240,9 +240,12 @@ private function endBundleFile(WriteInterface $bundleFile, array $contents)
private function getFileContent($sourcePath)
{
if (!isset($this->fileContent[$sourcePath])) {
- $this->fileContent[$sourcePath] = utf8_encode(
- $this->staticDir->readFile($this->minification->addMinifiedSign($sourcePath))
- );
+ $content = $this->staticDir->readFile($this->minification->addMinifiedSign($sourcePath));
+ if (mb_detect_encoding($content) !== "UTF-8") {
+ $content = mb_convert_encoding($content, "UTF-8");
+ }
+
+ $this->fileContent[$sourcePath] = $content;
}
return $this->fileContent[$sourcePath];
}
diff --git a/app/code/Magento/Deploy/Service/DeployPackage.php b/app/code/Magento/Deploy/Service/DeployPackage.php
index 0522702cbdc2b..ec80387e96cc9 100644
--- a/app/code/Magento/Deploy/Service/DeployPackage.php
+++ b/app/code/Magento/Deploy/Service/DeployPackage.php
@@ -3,14 +3,18 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Deploy\Service;
use Magento\Deploy\Package\Package;
use Magento\Deploy\Package\PackageFile;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\State as AppState;
use Magento\Framework\Locale\ResolverInterface as LocaleResolver;
use Magento\Framework\View\Asset\ContentProcessorException;
use Magento\Deploy\Console\InputValidator;
+use Magento\Framework\View\Design\Theme\ListInterface;
+use Magento\Framework\View\DesignInterface;
use Psr\Log\LoggerInterface;
/**
@@ -91,15 +95,15 @@ public function __construct(
* @param array $options
* @param bool $skipLogging
* @return bool true on success
+ * @throws \Exception
*/
public function deploy(Package $package, array $options, $skipLogging = false)
{
$result = $this->appState->emulateAreaCode(
- $package->getArea() == Package::BASE_AREA ? 'global' : $package->getArea(),
+ $package->getArea() === Package::BASE_AREA ? 'global' : $package->getArea(),
function () use ($package, $options, $skipLogging) {
// emulate application locale needed for correct file path resolving
$this->localeResolver->setLocale($package->getLocale());
-
$this->deployEmulated($package, $options, $skipLogging);
}
);
@@ -111,7 +115,7 @@ function () use ($package, $options, $skipLogging) {
* @param Package $package
* @param array $options
* @param bool $skipLogging
- * @return int
+ * @return bool
*/
public function deployEmulated(Package $package, array $options, $skipLogging = false)
{
@@ -200,14 +204,14 @@ private function processFile(PackageFile $file, Package $package)
private function checkIfCanCopy(PackageFile $file, Package $package, Package $parentPackage = null)
{
return $parentPackage
- && $file->getOrigPackage() !== $package
- && (
- $file->getArea() !== $package->getArea()
- || $file->getTheme() !== $package->getTheme()
- || $file->getLocale() !== $package->getLocale()
- )
- && $file->getOrigPackage() == $parentPackage
- && $this->deployStaticFile->readFile($file->getDeployedFileId(), $parentPackage->getPath());
+ && $file->getOrigPackage() !== $package
+ && (
+ $file->getArea() !== $package->getArea()
+ || $file->getTheme() !== $package->getTheme()
+ || $file->getLocale() !== $package->getLocale()
+ )
+ && $file->getOrigPackage() === $parentPackage
+ && $this->deployStaticFile->readFile($file->getDeployedFileId(), $parentPackage->getPath());
}
/**
@@ -219,10 +223,10 @@ private function checkIfCanCopy(PackageFile $file, Package $package, Package $pa
*/
private function checkFileSkip($filePath, array $options)
{
- if ($filePath != '.') {
+ if ($filePath !== '.') {
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$basename = pathinfo($filePath, PATHINFO_BASENAME);
- if ($ext == 'less' && strpos($basename, '_') === 0) {
+ if ($ext === 'less' && strpos($basename, '_') === 0) {
return true;
}
$option = isset(InputValidator::$fileExtensionOptionMap[$ext])
diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php
index 66ec6e7418afd..72de645868e22 100644
--- a/app/code/Magento/Deploy/Service/DeployStaticContent.php
+++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php
@@ -112,6 +112,7 @@ public function deploy(array $options)
$deployRjsConfig = $this->objectManager->create(DeployRequireJsConfig::class, [
'logger' => $this->logger
]);
+ /** @var DeployTranslationsDictionary $deployI18n */
$deployI18n = $this->objectManager->create(DeployTranslationsDictionary::class, [
'logger' => $this->logger
]);
diff --git a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php
index 673f31c04ffd3..d14c86c4a3264 100644
--- a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php
+++ b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php
@@ -5,47 +5,64 @@
*/
namespace Magento\Deploy\Test\Unit\Model;
+use Magento\Deploy\Model\Filesystem as DeployFilesystem;
+use Magento\Framework\Filesystem;
+use Magento\Framework\Filesystem\Directory\WriteInterface;
+use Magento\Framework\ObjectManagerInterface;
+use Magento\Framework\ShellInterface;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Store\Model\Config\StoreView;
+use Magento\User\Model\ResourceModel\User\Collection;
+use Magento\User\Model\User;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use Symfony\Component\Console\Output\OutputInterface;
+use Magento\Framework\Validator\Locale;
+use Magento\Framework\Setup\Lists;
+
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class FilesystemTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \Magento\Store\Model\Config\StoreView
+ * @var StoreView|MockObject
*/
- private $storeViewMock;
+ private $storeView;
/**
- * @var \Magento\Framework\ShellInterface
+ * @var ShellInterface|MockObject
*/
- private $shellMock;
+ private $shell;
/**
- * @var \Magento\User\Model\ResourceModel\User\Collection
+ * @var OutputInterface|MockObject
*/
- private $userCollectionMock;
+ private $output;
/**
- * @var \Symfony\Component\Console\Output\OutputInterface
+ * @var Filesystem|MockObject
*/
- private $outputMock;
+ private $filesystem;
/**
- * @var \Magento\Framework\Filesystem
+ * @var WriteInterface|MockObject
*/
- private $filesystemMock;
+ private $directoryWrite;
/**
- * @var \Magento\Framework\Filesystem\Directory\WriteInterface
+ * @var Collection|MockObject
*/
- private $directoryWriteMock;
+ private $userCollection;
/**
- * @var \Magento\Framework\ObjectManagerInterface
+ * @var ObjectManagerInterface|MockObject
*/
- private $objectManagerMock;
+ private $objectManager;
/**
- * @var \Magento\Deploy\Model\Filesystem
+ * @var DeployFilesystem
*/
- private $filesystem;
+ private $deployFilesystem;
/**
* @var string
@@ -54,75 +71,127 @@ class FilesystemTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
-
- $this->storeViewMock = $this->createMock(\Magento\Store\Model\Config\StoreView::class);
- $this->shellMock = $this->createMock(\Magento\Framework\ShellInterface::class);
- $this->userCollectionMock = $this->createMock(\Magento\User\Model\ResourceModel\User\Collection::class);
- $this->outputMock = $this->createMock(\Symfony\Component\Console\Output\OutputInterface::class);
- $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class);
- $this->filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class);
- $this->directoryWriteMock = $this->createMock(\Magento\Framework\Filesystem\Directory\WriteInterface::class);
- $this->filesystemMock->expects($this->any())
- ->method('getDirectoryWrite')
- ->willReturn($this->directoryWriteMock);
- $this->filesystem = $objectManager->getObject(
- \Magento\Deploy\Model\Filesystem::class,
+ $objectManager = new ObjectManager($this);
+
+ $this->storeView = $this->createMock(StoreView::class);
+ $this->shell = $this->createMock(ShellInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ $this->objectManager = $this->createMock(ObjectManagerInterface::class);
+ $this->filesystem = $this->createMock(Filesystem::class);
+ $this->directoryWrite = $this->createMock(WriteInterface::class);
+ $this->filesystem->method('getDirectoryWrite')
+ ->willReturn($this->directoryWrite);
+
+ $this->userCollection = $this->createMock(Collection::class);
+
+ $lists = $this->getMockBuilder(Lists::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lists->method('getLocaleList')
+ ->willReturn([
+ 'fr_FR' => 'France',
+ 'de_DE' => 'Germany',
+ 'nl_NL' => 'Netherlands',
+ 'en_US' => 'USA'
+ ]);
+ $locale = $objectManager->getObject(Locale::class, ['lists' => $lists]);
+
+ $this->deployFilesystem = $objectManager->getObject(
+ DeployFilesystem::class,
[
- 'storeView' => $this->storeViewMock,
- 'shell' => $this->shellMock,
- 'filesystem' => $this->filesystemMock
+ 'storeView' => $this->storeView,
+ 'shell' => $this->shell,
+ 'filesystem' => $this->filesystem,
+ 'userCollection' => $this->userCollection,
+ 'locale' => $locale
]
);
- $userCollection = new \ReflectionProperty(\Magento\Deploy\Model\Filesystem::class, 'userCollection');
- $userCollection->setAccessible(true);
- $userCollection->setValue($this->filesystem, $this->userCollectionMock);
-
$this->cmdPrefix = PHP_BINARY . ' -f ' . BP . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'magento ';
}
public function testRegenerateStatic()
{
$storeLocales = ['fr_FR', 'de_DE', 'nl_NL'];
- $adminUserInterfaceLocales = ['de_DE', 'en_US'];
- $this->storeViewMock->expects($this->once())
- ->method('retrieveLocales')
+ $this->storeView->method('retrieveLocales')
->willReturn($storeLocales);
- $userMock = $this->createMock(\Magento\User\Model\User::class);
- $userMock->expects($this->once())
- ->method('getInterfaceLocale')
- ->willReturn('en_US');
- $this->userCollectionMock->expects($this->once())
- ->method('getIterator')
- ->willReturn(new \ArrayIterator([$userMock]));
-
- $usedLocales = array_unique(
- array_merge($storeLocales, $adminUserInterfaceLocales)
- );
- $staticContentDeployCmd = $this->cmdPrefix . 'setup:static-content:deploy -f '
- . implode(' ', $usedLocales);
+
$setupDiCompileCmd = $this->cmdPrefix . 'setup:di:compile';
- $this->shellMock->expects($this->at(0))
+ $this->shell->expects(self::at(0))
->method('execute')
->with($setupDiCompileCmd);
- $this->shellMock->expects($this->at(1))
+
+ $this->initAdminLocaleMock('en_US');
+
+ $usedLocales = ['fr_FR', 'de_DE', 'nl_NL', 'en_US'];
+ $staticContentDeployCmd = $this->cmdPrefix . 'setup:static-content:deploy -f '
+ . implode(' ', $usedLocales);
+ $this->shell->expects(self::at(1))
->method('execute')
->with($staticContentDeployCmd);
- $this->outputMock->expects($this->at(0))
+ $this->output->expects(self::at(0))
->method('writeln')
->with('Starting compilation');
- $this->outputMock->expects($this->at(2))
+ $this->output->expects(self::at(2))
->method('writeln')
->with('Compilation complete');
- $this->outputMock->expects($this->at(3))
+ $this->output->expects(self::at(3))
->method('writeln')
->with('Starting deployment of static content');
- $this->outputMock->expects($this->at(5))
+ $this->output->expects(self::at(5))
->method('writeln')
->with('Deployment of static content complete');
- $this->filesystem->regenerateStatic($this->outputMock);
+ $this->deployFilesystem->regenerateStatic($this->output);
+ }
+
+ /**
+ * Checks a case when configuration contains incorrect locale code.
+ *
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales
+ */
+ public function testGenerateStaticForNotAllowedStoreViewLocale()
+ {
+ $storeLocales = ['fr_FR', 'de_DE', ';echo'];
+ $this->storeView->method('retrieveLocales')
+ ->willReturn($storeLocales);
+
+ $this->initAdminLocaleMock('en_US');
+
+ $this->deployFilesystem->regenerateStatic($this->output);
+ }
+
+ /**
+ * Checks as case when admin locale is incorrect.
+ *
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales
+ */
+ public function testGenerateStaticForNotAllowedAdminLocale()
+ {
+ $storeLocales = ['fr_FR', 'de_DE', 'en_US'];
+ $this->storeView->method('retrieveLocales')
+ ->willReturn($storeLocales);
+
+ $this->initAdminLocaleMock(';echo');
+
+ $this->deployFilesystem->regenerateStatic($this->output);
+ }
+
+ /**
+ * Initializes admin user locale.
+ *
+ * @param string $locale
+ */
+ private function initAdminLocaleMock($locale)
+ {
+ /** @var User|MockObject $user */
+ $user = $this->createMock(User::class);
+ $user->method('getInterfaceLocale')
+ ->willReturn($locale);
+ $this->userCollection->method('getIterator')
+ ->willReturn(new \ArrayIterator([$user]));
}
}
diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json
index 8092f80be0e1a..e33d2e72f64a4 100644
--- a/app/code/Magento/Deploy/composer.json
+++ b/app/code/Magento/Deploy/composer.json
@@ -10,7 +10,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Developer/Console/Command/ProfilerDisableCommand.php b/app/code/Magento/Developer/Console/Command/ProfilerDisableCommand.php
new file mode 100644
index 0000000000000..40916703ed28f
--- /dev/null
+++ b/app/code/Magento/Developer/Console/Command/ProfilerDisableCommand.php
@@ -0,0 +1,74 @@
+filesystem = $filesystem;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setDescription('Disable the profiler.');
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws \InvalidArgumentException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->filesystem->rm(BP . '/' . self::PROFILER_FLAG_FILE);
+ if (!$this->filesystem->fileExists(BP . '/' . self::PROFILER_FLAG_FILE)) {
+ $output->writeln(''. self::SUCCESS_MESSAGE . ' ');
+ return;
+ }
+ $output->writeln('Something went wrong while disabling the profiler. ');
+ }
+}
diff --git a/app/code/Magento/Developer/Console/Command/ProfilerEnableCommand.php b/app/code/Magento/Developer/Console/Command/ProfilerEnableCommand.php
new file mode 100644
index 0000000000000..0917f55cd1bdc
--- /dev/null
+++ b/app/code/Magento/Developer/Console/Command/ProfilerEnableCommand.php
@@ -0,0 +1,107 @@
+filesystem = $filesystem;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setDescription('Enable the profiler.')
+ ->addArgument('type', InputArgument::OPTIONAL, 'Profiler type', self::TYPE_DEFAULT);
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws \InvalidArgumentException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $type = $input->getArgument('type');
+ if (!in_array($type, self::BUILT_IN_TYPES)) {
+ $builtInTypes = implode(', ', self::BUILT_IN_TYPES);
+ $output->writeln(
+ '' . sprintf('Type %s is not one of the built-in output types (%s).', $type) .
+ sprintf('Make sure the necessary class exists.', $type, $builtInTypes) . ' '
+ );
+ }
+
+ $this->filesystem->write(BP . '/' . self::PROFILER_FLAG_FILE, $type);
+ if ($this->filesystem->fileExists(BP . '/' . self::PROFILER_FLAG_FILE)) {
+ $output->write(''. sprintf(self::SUCCESS_MESSAGE, $type) . ' ');
+ if ($type == 'csvfile') {
+ $output->write(
+ ' ' . sprintf(
+ 'Output will be saved in %s',
+ \Magento\Framework\Profiler\Driver\Standard\Output\Csvfile::DEFAULT_FILEPATH
+ )
+ . ' '
+ );
+ }
+ $output->write(PHP_EOL);
+ return;
+ }
+
+ $output->writeln('Something went wrong while enabling the profiler. ');
+ }
+}
diff --git a/app/code/Magento/Developer/composer.json b/app/code/Magento/Developer/composer.json
index a0782be817c0a..518f2cc77c07e 100644
--- a/app/code/Magento/Developer/composer.json
+++ b/app/code/Magento/Developer/composer.json
@@ -8,7 +8,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Developer/etc/di.xml b/app/code/Magento/Developer/etc/di.xml
index ca35c38a31b68..85b28c90132af 100644
--- a/app/code/Magento/Developer/etc/di.xml
+++ b/app/code/Magento/Developer/etc/di.xml
@@ -102,6 +102,8 @@
- Magento\Developer\Console\Command\QueryLogDisableCommand
- Magento\Developer\Console\Command\TemplateHintsDisableCommand
- Magento\Developer\Console\Command\TemplateHintsEnableCommand
+ - Magento\Developer\Console\Command\ProfilerDisableCommand
+ - Magento\Developer\Console\Command\ProfilerEnableCommand
diff --git a/app/code/Magento/Directory/Model/Currency.php b/app/code/Magento/Directory/Model/Currency.php
index a8df4936b8fae..0b5b836b4ac93 100644
--- a/app/code/Magento/Directory/Model/Currency.php
+++ b/app/code/Magento/Directory/Model/Currency.php
@@ -6,6 +6,7 @@
namespace Magento\Directory\Model;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\InputException;
use Magento\Directory\Model\Currency\Filter;
@@ -65,6 +66,11 @@ class Currency extends \Magento\Framework\Model\AbstractModel
*/
protected $_localeCurrency;
+ /**
+ * @var CurrencyConfig
+ */
+ private $currencyConfig;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -76,6 +82,7 @@ class Currency extends \Magento\Framework\Model\AbstractModel
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param CurrencyConfig $currencyConfig
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -88,7 +95,8 @@ public function __construct(
\Magento\Framework\Locale\CurrencyInterface $localeCurrency,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ CurrencyConfig $currencyConfig = null
) {
parent::__construct(
$context,
@@ -102,6 +110,7 @@ public function __construct(
$this->_directoryHelper = $directoryHelper;
$this->_currencyFilterFactory = $currencyFilterFactory;
$this->_localeCurrency = $localeCurrency;
+ $this->currencyConfig = $currencyConfig ?: ObjectManager::getInstance()->get(CurrencyConfig::class);
}
/**
@@ -347,7 +356,7 @@ public function getOutputFormat()
*/
public function getConfigAllowCurrencies()
{
- $allowedCurrencies = $this->_getResource()->getConfigCurrencies($this, self::XML_PATH_CURRENCY_ALLOW);
+ $allowedCurrencies = $this->currencyConfig->getConfigCurrencies(self::XML_PATH_CURRENCY_ALLOW);
$appBaseCurrencyCode = $this->_directoryHelper->getBaseCurrencyCode();
if (!in_array($appBaseCurrencyCode, $allowedCurrencies)) {
$allowedCurrencies[] = $appBaseCurrencyCode;
@@ -369,8 +378,7 @@ public function getConfigAllowCurrencies()
*/
public function getConfigDefaultCurrencies()
{
- $defaultCurrencies = $this->_getResource()->getConfigCurrencies($this, self::XML_PATH_CURRENCY_DEFAULT);
- return $defaultCurrencies;
+ return $this->currencyConfig->getConfigCurrencies(self::XML_PATH_CURRENCY_DEFAULT);
}
/**
@@ -378,8 +386,7 @@ public function getConfigDefaultCurrencies()
*/
public function getConfigBaseCurrencies()
{
- $defaultCurrencies = $this->_getResource()->getConfigCurrencies($this, self::XML_PATH_CURRENCY_BASE);
- return $defaultCurrencies;
+ return $this->currencyConfig->getConfigCurrencies(self::XML_PATH_CURRENCY_BASE);
}
/**
diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php
new file mode 100644
index 0000000000000..fdb561c224170
--- /dev/null
+++ b/app/code/Magento/Directory/Model/CurrencyConfig.php
@@ -0,0 +1,99 @@
+appState = $appState;
+ $this->config = $config;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Retrieve config currency data by config path.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getConfigCurrencies(string $path)
+ {
+ $result = $this->appState->getAreaCode() === Area::AREA_ADMINHTML
+ ? $this->getConfigForAllStores($path)
+ : $this->getConfigForCurrentStore($path);
+ sort($result);
+
+ return array_unique($result);
+ }
+
+ /**
+ * Get allowed, base and default currency codes for all stores.
+ *
+ * @param string $path
+ * @return array
+ */
+ private function getConfigForAllStores(string $path)
+ {
+ $storesResult = [[]];
+ foreach ($this->storeManager->getStores() as $store) {
+ $storesResult[] = explode(
+ ',',
+ $this->config->getValue($path, ScopeInterface::SCOPE_STORE, $store->getCode())
+ );
+ }
+
+ return array_merge(...$storesResult);
+ }
+
+ /**
+ * Get allowed, base and default currency codes for current store.
+ *
+ * @param string $path
+ * @return mixed
+ */
+ private function getConfigForCurrentStore(string $path)
+ {
+ $store = $this->storeManager->getStore();
+
+ return explode(',', $this->config->getValue($path, ScopeInterface::SCOPE_STORE, $store->getCode()));
+ }
+}
diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php
index ac0716fc4e67e..ffbcce11cb4f6 100644
--- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php
+++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php
@@ -165,6 +165,8 @@ public function saveRates($rates)
* @param string $path
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @deprecated because doesn't take into consideration scopes and system config values.
+ * @see \Magento\Directory\Model\CurrencyConfig::getConfigCurrencies()
*/
public function getConfigCurrencies($model, $path)
{
diff --git a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php
new file mode 100644
index 0000000000000..9b52bae26f90f
--- /dev/null
+++ b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php
@@ -0,0 +1,127 @@
+config = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
+ ->setMethods(['getStores', 'getWebsites'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->appState = $this->getMockBuilder(State::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManager = new ObjectManager($this);
+ $this->testSubject = $objectManager->getObject(
+ CurrencyConfig::class,
+ [
+ 'storeManager' => $this->storeManager,
+ 'appState' => $this->appState,
+ 'config' => $this->config,
+ ]
+ );
+ }
+
+ /**
+ * Test get currency config for admin and storefront areas.
+ *
+ * @dataProvider getConfigCurrenciesDataProvider
+ * @return void
+ */
+ public function testGetConfigCurrencies(string $areCode)
+ {
+ $path = 'test/path';
+ $expected = ['ARS', 'AUD', 'BZD'];
+
+ $this->appState->expects(self::once())
+ ->method('getAreaCode')
+ ->willReturn($areCode);
+
+ /** @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject $store */
+ $store = $this->getMockBuilder(StoreInterface::class)
+ ->setMethods(['getCode'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $store->expects(self::once())
+ ->method('getCode')
+ ->willReturn('testCode');
+
+ if ($areCode === Area::AREA_ADMINHTML) {
+ $this->storeManager->expects(self::once())
+ ->method('getStores')
+ ->willReturn([$store]);
+ } else {
+ $this->storeManager->expects(self::once())
+ ->method('getStore')
+ ->willReturn($store);
+ }
+
+ $this->config->expects(self::once())
+ ->method('getValue')
+ ->with(
+ self::identicalTo($path)
+ )->willReturn('ARS,AUD,BZD');
+
+ $result = $this->testSubject->getConfigCurrencies($path);
+
+ self::assertEquals($expected, $result);
+ }
+
+ /**
+ * Provide test data for getConfigCurrencies test.
+ *
+ * @return array
+ */
+ public function getConfigCurrenciesDataProvider()
+ {
+ return [
+ ['areaCode' => Area::AREA_ADMINHTML],
+ ['areaCode' => Area::AREA_FRONTEND],
+ ];
+ }
+}
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php
index f47c73f01f238..5ea8e97e7004f 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php
@@ -58,7 +58,7 @@ public function afterLoad($object)
{
parent::afterLoad($object);
$attrCode = $this->getAttribute()->getAttributeCode();
- $object->setData($attrCode, $this->jsonSerializer->unserialize($object->getData($attrCode)));
+ $object->setData($attrCode, $this->jsonSerializer->unserialize($object->getData($attrCode) ?: '{}'));
return $this;
}
}
diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/JsonEncodedTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/JsonEncodedTest.php
index 96846c0d1fe56..d94d25e7fd180 100644
--- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/JsonEncodedTest.php
+++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/JsonEncodedTest.php
@@ -95,4 +95,18 @@ public function testAfterLoad()
$this->model->afterLoad($product);
$this->assertEquals([1, 2, 3], $product->getData('json_encoded'));
}
+
+ /**
+ * Test after load handler with null attribute value
+ */
+ public function testAfterLoadWithNullAttributeValue()
+ {
+ $product = new \Magento\Framework\DataObject(
+ [
+ 'json_encoded' => null
+ ]
+ );
+ $this->model->afterLoad($product);
+ $this->assertEquals([], $product->getData('json_encoded'));
+ }
}
diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php
index 8bfa2fc7d64cc..2eb8f582c54fa 100644
--- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php
+++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php
@@ -369,7 +369,7 @@ public function applyInlineCssDataProvider()
'
',
'p { color: #000 }',
[
- '',
+ '',
'
',
],
],
diff --git a/app/code/Magento/GoogleAnalytics/composer.json b/app/code/Magento/GoogleAnalytics/composer.json
index bf7eadd9f708f..b74763a22ec14 100644
--- a/app/code/Magento/GoogleAnalytics/composer.json
+++ b/app/code/Magento/GoogleAnalytics/composer.json
@@ -12,7 +12,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
index 91cfbefbd57a2..e490ee4018376 100644
--- a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
+++ b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
@@ -39,7 +39,7 @@ public function __construct(
*/
public function execute()
{
- $fileName = $this->getRequest()->getParam('filename');
+ $fileName = basename($this->getRequest()->getParam('filename'));
/** @var \Magento\ImportExport\Helper\Report $reportHelper */
$reportHelper = $this->_objectManager->get(\Magento\ImportExport\Helper\Report::class);
diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php
index c61292f7c34f7..e7883693fbe74 100644
--- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php
+++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php
@@ -7,6 +7,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\ImportExport\Model\Import\AbstractSource;
use Magento\ImportExport\Model\Import as ImportExport;
@@ -310,7 +311,7 @@ public function __construct(
protected function _getSource()
{
if (!$this->_source) {
- throw new \Magento\Framework\Exception\LocalizedException(__('Please specify a source.'));
+ throw new LocalizedException(__('Please specify a source.'));
}
return $this->_source;
}
@@ -378,7 +379,7 @@ protected function addErrors($code, $errors)
/**
* Validate data rows and save bunches to DB.
*
- * @return $this|void
+ * @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
protected function _saveValidatedBunches()
@@ -548,11 +549,11 @@ public function getBehavior()
if (!isset(
$this->_parameters['behavior']
) ||
- $this->_parameters['behavior'] != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND &&
- $this->_parameters['behavior'] != \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE &&
- $this->_parameters['behavior'] != \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE
+ $this->_parameters['behavior'] != ImportExport::BEHAVIOR_APPEND &&
+ $this->_parameters['behavior'] != ImportExport::BEHAVIOR_REPLACE &&
+ $this->_parameters['behavior'] != ImportExport::BEHAVIOR_DELETE
) {
- return \Magento\ImportExport\Model\Import::getDefaultBehavior();
+ return ImportExport::getDefaultBehavior();
}
return $this->_parameters['behavior'];
}
@@ -604,7 +605,7 @@ public function getProcessedRowsCount()
public function getSource()
{
if (!$this->_source) {
- throw new \Magento\Framework\Exception\LocalizedException(__('The source is not set.'));
+ throw new LocalizedException(__('The source is not set.'));
}
return $this->_source;
}
@@ -879,7 +880,7 @@ public function getValidColumnNames()
protected function getMetadataPool()
{
if (!$this->metadataPool) {
- $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
+ $this->metadataPool = ObjectManager::getInstance()
->get(\Magento\Framework\EntityManager\MetadataPool::class);
}
return $this->metadataPool;
diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
index 73b97dfc67751..3eb14e673f648 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
@@ -72,8 +72,9 @@ class DownloadTest extends \PHPUnit\Framework\TestCase
*/
protected function setUp()
{
- $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam']);
- $this->request->expects($this->any())->method('getParam')->with('filename')->willReturn('filename');
+ $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->reportHelper = $this->createPartialMock(
\Magento\ImportExport\Helper\Report::class,
['importFileExists', 'getReportSize', 'getReportOutput']
@@ -126,11 +127,12 @@ protected function setUp()
}
/**
- * Test execute()
+ * Tests execute()
*/
public function testExecute()
{
- $this->reportHelper->expects($this->any())->method('importFileExists')->willReturn(true);
+ $this->reportHelper->expects($this->atLeastOnce())->method('importFileExists')->willReturn(true);
+
$this->resultRaw->expects($this->once())->method('setContents');
$this->downloadController->execute();
}
@@ -140,7 +142,8 @@ public function testExecute()
*/
public function testExecuteFileNotFound()
{
- $this->reportHelper->expects($this->any())->method('importFileExists')->willReturn(false);
+ $this->request->method('getParam')->with('filename')->willReturn('filename');
+ $this->reportHelper->method('importFileExists')->willReturn(false);
$this->resultRaw->expects($this->never())->method('setContents');
$this->downloadController->execute();
}
diff --git a/app/code/Magento/ImportExport/composer.json b/app/code/Magento/ImportExport/composer.json
index 623346e0a7782..23ed2f6346ef7 100644
--- a/app/code/Magento/ImportExport/composer.json
+++ b/app/code/Magento/ImportExport/composer.json
@@ -12,7 +12,7 @@
"ext-ctype": "*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CarrierFinder.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CarrierFinder.php
new file mode 100644
index 0000000000000..0eb2afd04140a
--- /dev/null
+++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CarrierFinder.php
@@ -0,0 +1,63 @@
+carriersConfig = $carriersConfig;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Finds carriers delivering to customer address
+ *
+ * @param Address $address
+ * @return array
+ */
+ public function getCarriersForCustomerAddress(Address $address): array
+ {
+ $request = new DataObject([
+ 'dest_country_id' => $address->getCountryId()
+ ]);
+
+ $carriers = [];
+ foreach ($this->carriersConfig->getActiveCarriers($this->storeManager->getStore()->getId()) as $carrier) {
+ $checked = $carrier->checkAvailableShipCountries($request);
+ if (false !== $checked && null === $checked->getErrorMessage() && !empty($checked->getAllowedMethods())) {
+ $carriers[] = $checked;
+ }
+ }
+
+ return $carriers;
+ }
+}
diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CheapestMethodChooser.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CheapestMethodChooser.php
index a9abf09c8b22c..f01bde29cbc7d 100644
--- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CheapestMethodChooser.php
+++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/CheapestMethodChooser.php
@@ -6,7 +6,6 @@
namespace Magento\InstantPurchase\Model\ShippingMethodChoose;
use Magento\Customer\Model\Address;
-use Magento\Quote\Api\Data\ShippingMethodInterface;
use Magento\Quote\Api\Data\ShippingMethodInterfaceFactory;
/**
@@ -20,21 +19,21 @@ class CheapestMethodChooser implements ShippingMethodChooserInterface
private $shippingMethodFactory;
/**
- * @var ShippingRateFinder
+ * @var CarrierFinder
*/
- private $shippingRateFinder;
+ private $carrierFinder;
/**
* CheapestMethodChooser constructor.
* @param ShippingMethodInterfaceFactory $shippingMethodFactory
- * @param ShippingRateFinder $shippingRateFinder
+ * @param CarrierFinder $carrierFinder
*/
public function __construct(
ShippingMethodInterfaceFactory $shippingMethodFactory,
- ShippingRateFinder $shippingRateFinder
+ CarrierFinder $carrierFinder
) {
$this->shippingMethodFactory = $shippingMethodFactory;
- $this->shippingRateFinder = $shippingRateFinder;
+ $this->carrierFinder = $carrierFinder;
}
/**
@@ -58,7 +57,7 @@ public function choose(Address $address)
*/
private function areShippingMethodsAvailable(Address $address): bool
{
- $shippingRatesForAddress = $this->shippingRateFinder->getRatesForCustomerAddress($address);
- return !empty($shippingRatesForAddress);
+ $carriersForAddress = $this->carrierFinder->getCarriersForCustomerAddress($address);
+ return !empty($carriersForAddress);
}
}
diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingRateFinder.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingRateFinder.php
deleted file mode 100644
index 065f3fe946e71..0000000000000
--- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingRateFinder.php
+++ /dev/null
@@ -1,81 +0,0 @@
-rateRequestFactory = $rateRequestFactory;
- $this->storeManager = $storeManager;
- $this->rateCollectorFactory = $rateCollectorFactory;
- }
-
- /**
- * Finds shipping rates for an address.
- *
- * @param Address $address
- * @return array
- */
- public function getRatesForCustomerAddress(Address $address): array
- {
- /** @var $request RateRequest */
- $request = $this->rateRequestFactory->create();
- $request->setDestCountryId($address->getCountryId());
- $request->setDestRegionId($address->getRegionId());
- $request->setDestRegionCode($address->getRegionCode());
- $request->setDestStreet($address->getStreetFull());
- $request->setDestCity($address->getCity());
- $request->setDestPostcode($address->getPostcode());
- $request->setStoreId($this->storeManager->getStore()->getId());
- $request->setWebsiteId($this->storeManager->getWebsite()->getId());
- $request->setBaseCurrency($this->storeManager->getStore()->getBaseCurrency());
- $request->setPackageCurrency($this->storeManager->getStore()->getCurrentCurrency());
- // Because of wrong compare operator in \Magento\OfflineShipping\Model\Carrier\Tablerate on line: 167
- $request->setPackageQty(-1);
-
- $result = $this->rateCollectorFactory->create()->collectRates($request)->getResult();
-
- $shippingRates = [];
-
- if ($result) {
- $shippingRates = $result->getAllRates();
- }
-
- return $shippingRates;
- }
-}
diff --git a/app/code/Magento/InstantPurchase/composer.json b/app/code/Magento/InstantPurchase/composer.json
index df13e10f55b8f..1c3b97a6beb94 100644
--- a/app/code/Magento/InstantPurchase/composer.json
+++ b/app/code/Magento/InstantPurchase/composer.json
@@ -13,6 +13,7 @@
"magento/module-catalog": "102.0.*",
"magento/module-customer": "101.0.*",
"magento/module-sales": "101.0.*",
+ "magento/module-shipping": "100.2.*",
"magento/module-quote": "101.0.*",
"magento/module-vault": "101.0.*",
"magento/framework": "100.2.*"
diff --git a/app/code/Magento/Integration/etc/acl.xml b/app/code/Magento/Integration/etc/acl.xml
new file mode 100644
index 0000000000000..51eb078bd1df7
--- /dev/null
+++ b/app/code/Magento/Integration/etc/acl.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php
index c4818c38cd9c6..845ed0429d2c3 100644
--- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php
+++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php
@@ -28,6 +28,19 @@ public function addCustomParameter($param, $value)
return false;
}
+ /**
+ * Wrapper for 'newrelic_notice_error' function
+ *
+ * @param Exception $exception
+ * @return void
+ */
+ public function reportError($exception)
+ {
+ if (extension_loaded('newrelic')) {
+ newrelic_notice_error($exception->getMessage(), $exception);
+ }
+ }
+
/**
* Checks whether newrelic-php5 agent is installed
*
diff --git a/app/code/Magento/NewRelicReporting/Plugin/HttpPlugin.php b/app/code/Magento/NewRelicReporting/Plugin/HttpPlugin.php
new file mode 100644
index 0000000000000..a37d93329d43a
--- /dev/null
+++ b/app/code/Magento/NewRelicReporting/Plugin/HttpPlugin.php
@@ -0,0 +1,53 @@
+config = $config;
+ $this->newRelicWrapper = $newRelicWrapper;
+ }
+
+ /**
+ * Report exception to New Relic
+ *
+ * @param Http $subject
+ * @param Bootstrap $bootstrap
+ * @param \Exception $exception
+ * @return void
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeCatchException(Http $subject, Bootstrap $bootstrap, \Exception $exception)
+ {
+ if ($this->config->isNewRelicEnabled()) {
+ $this->newRelicWrapper->reportError($exception);
+ }
+ }
+}
diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml
index a0d06105dd3fe..cba92f91cd4bb 100644
--- a/app/code/Magento/NewRelicReporting/etc/di.xml
+++ b/app/code/Magento/NewRelicReporting/etc/di.xml
@@ -27,4 +27,7 @@
+
+
+
diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
index c72ae42031001..c7ce4b2f2f11b 100644
--- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
@@ -118,17 +118,37 @@ public function loadByEmail($subscriberEmail)
*/
public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer)
{
- $select = $this->connection->select()->from($this->getMainTable())->where('customer_id=:customer_id');
-
- $result = $this->connection->fetchRow($select, ['customer_id' => $customer->getId()]);
+ $select = $this->connection
+ ->select()
+ ->from($this->getMainTable())
+ ->where('customer_id=:customer_id and store_id=:store_id');
+
+ $result = $this->connection
+ ->fetchRow(
+ $select,
+ [
+ 'customer_id' => $customer->getId(),
+ 'store_id' => $customer->getStoreId()
+ ]
+ );
if ($result) {
return $result;
}
- $select = $this->connection->select()->from($this->getMainTable())->where('subscriber_email=:subscriber_email');
-
- $result = $this->connection->fetchRow($select, ['subscriber_email' => $customer->getEmail()]);
+ $select = $this->connection
+ ->select()
+ ->from($this->getMainTable())
+ ->where('subscriber_email=:subscriber_email and store_id=:store_id');
+
+ $result = $this->connection
+ ->fetchRow(
+ $select,
+ [
+ 'subscriber_email' => $customer->getEmail(),
+ 'store_id' => $customer->getStoreId()
+ ]
+ );
if ($result) {
return $result;
diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php
index 9e021a21d23b3..cc143fdc52e3b 100644
--- a/app/code/Magento/Newsletter/Model/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/Subscriber.php
@@ -7,8 +7,10 @@
use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\MailException;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Stdlib\DateTime\DateTime;
/**
* Subscriber model
@@ -94,6 +96,12 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel
*/
protected $_customerSession;
+ /**
+ * Date
+ * @var DateTime
+ */
+ private $dateTime;
+
/**
* Store manager
*
@@ -134,9 +142,10 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel
* @param CustomerRepositoryInterface $customerRepository
* @param AccountManagementInterface $customerAccountManagement
* @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
- * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
- * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
+ * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
+ * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
* @param array $data
+ * @param DateTime|null $dateTime
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -152,7 +161,8 @@ public function __construct(
\Magento\Framework\Translate\Inline\StateInterface $inlineTranslation,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ DateTime $dateTime = null
) {
$this->_newsletterData = $newsletterData;
$this->_scopeConfig = $scopeConfig;
@@ -162,6 +172,7 @@ public function __construct(
$this->customerRepository = $customerRepository;
$this->customerAccountManagement = $customerAccountManagement;
$this->inlineTranslation = $inlineTranslation;
+ $this->dateTime = $dateTime ?: ObjectManager::getInstance()->get(DateTime::class);
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
@@ -349,6 +360,7 @@ public function loadByCustomerId($customerId)
{
try {
$customerData = $this->customerRepository->getById($customerId);
+ $customerData->setStoreId($this->_storeManager->getStore()->getId());
$data = $this->getResource()->loadByCustomerData($customerData);
$this->addData($data);
if (!empty($data) && $customerData->getId() && !$this->getCustomerId()) {
@@ -810,4 +822,18 @@ public function getSubscriberFullName()
}
return $name;
}
+
+ /**
+ * Set date of last changed status
+ *
+ * @return $this
+ */
+ public function beforeSave()
+ {
+ parent::beforeSave();
+ if ($this->dataHasChangedFor('subscriber_status')) {
+ $this->setChangeStatusAt($this->dateTime->gmtDate());
+ }
+ return $this;
+ }
}
diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
index 7716f4744a922..5a4032dc4dffd 100644
--- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
+++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
@@ -187,6 +187,12 @@ public function testUpdateSubscription()
$customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id');
$customerDataMock->expects($this->once())->method('getEmail')->willReturn('email');
+ $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getId'])
+ ->getMock();
+ $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel);
+
$this->assertEquals($this->subscriber, $this->subscriber->updateSubscription($customerId));
}
diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl
index 3e8acfba18762..9e57f1e362320 100644
--- a/app/code/Magento/PageCache/etc/varnish4.vcl
+++ b/app/code/Magento/PageCache/etc/varnish4.vcl
@@ -141,6 +141,10 @@ sub vcl_backend_response {
set beresp.do_gzip = true;
}
+ if (beresp.http.X-Magento-Debug) {
+ set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
+ }
+
# cache only successfully responses and 404s
if (beresp.status != 200 && beresp.status != 404) {
set beresp.ttl = 0s;
@@ -152,10 +156,6 @@ sub vcl_backend_response {
return (deliver);
}
- if (beresp.http.X-Magento-Debug) {
- set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
- }
-
# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
unset beresp.http.set-cookie;
@@ -163,12 +163,15 @@ sub vcl_backend_response {
# If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if (beresp.ttl <= 0s ||
- beresp.http.Surrogate-control ~ "no-store" ||
- (!beresp.http.Surrogate-Control && beresp.http.Vary == "*")) {
- # Mark as Hit-For-Pass for the next 2 minutes
+ beresp.http.Surrogate-control ~ "no-store" ||
+ (!beresp.http.Surrogate-Control &&
+ beresp.http.Cache-Control ~ "no-cache|no-store") ||
+ beresp.http.Vary == "*") {
+ # Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
set beresp.uncacheable = true;
}
+
return (deliver);
}
diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl
index c060090aa91ed..cfc2192688748 100644
--- a/app/code/Magento/PageCache/etc/varnish5.vcl
+++ b/app/code/Magento/PageCache/etc/varnish5.vcl
@@ -142,6 +142,10 @@ sub vcl_backend_response {
set beresp.do_gzip = true;
}
+ if (beresp.http.X-Magento-Debug) {
+ set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
+ }
+
# cache only successfully responses and 404s
if (beresp.status != 200 && beresp.status != 404) {
set beresp.ttl = 0s;
@@ -153,10 +157,6 @@ sub vcl_backend_response {
return (deliver);
}
- if (beresp.http.X-Magento-Debug) {
- set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
- }
-
# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
unset beresp.http.set-cookie;
@@ -164,12 +164,15 @@ sub vcl_backend_response {
# If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if (beresp.ttl <= 0s ||
- beresp.http.Surrogate-control ~ "no-store" ||
- (!beresp.http.Surrogate-Control && beresp.http.Vary == "*")) {
+ beresp.http.Surrogate-control ~ "no-store" ||
+ (!beresp.http.Surrogate-Control &&
+ beresp.http.Cache-Control ~ "no-cache|no-store") ||
+ beresp.http.Vary == "*") {
# Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
set beresp.uncacheable = true;
}
+
return (deliver);
}
diff --git a/app/code/Magento/Payment/Helper/Data.php b/app/code/Magento/Payment/Helper/Data.php
index e3122913d5dfa..f3565ea324290 100644
--- a/app/code/Magento/Payment/Helper/Data.php
+++ b/app/code/Magento/Payment/Helper/Data.php
@@ -293,6 +293,7 @@ public function getPaymentMethodList($sorted = true, $asLabelValue = false, $wit
foreach ($methods as $code => $title) {
if (isset($groups[$code])) {
$labelValues[$code]['label'] = $title;
+ $labelValues[$code]['value'] = null;
} elseif (isset($groupRelations[$code])) {
unset($labelValues[$code]);
$labelValues[$groupRelations[$code]]['value'][$code] = ['value' => $code, 'label' => $title];
diff --git a/app/code/Magento/Paypal/Model/Config.php b/app/code/Magento/Paypal/Model/Config.php
index acb0444d50803..08b646a661898 100644
--- a/app/code/Magento/Paypal/Model/Config.php
+++ b/app/code/Magento/Paypal/Model/Config.php
@@ -226,6 +226,7 @@ class Config extends AbstractConfig
'TWD',
'THB',
'USD',
+ 'INR',
];
/**
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js
index 4d68ad8ef9f4b..c56f21bc718fb 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js
@@ -78,7 +78,9 @@ define(
$('body').trigger('processStop');
customerData.invalidate(['cart']);
});
- }.bind(this));
+ }.bind(this)).fail(function () {
+ paypalExpressCheckout.checkout.closeFlow();
+ });
}
}
}
diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
index 03ce42bf25c4a..1dfcc95a552c6 100644
--- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
+++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
@@ -134,7 +134,6 @@ define([
*/
_create: function () {
$(this.element).on('gallery:loaded', $.proxy(function () {
- this.fotoramaItem = $(this.element).find('.fotorama-item');
this._initialize();
}, this));
},
@@ -154,6 +153,7 @@ define([
this.defaultVideoData = this.options.videoData = this.videoDataPlaceholder;
}
+ this.fotoramaItem = $(this.element).find('.fotorama-item');
this.clearEvents();
if (this._checkForVideoExist()) {
@@ -164,6 +164,8 @@ define([
this._initFotoramaVideo();
this._attachFotoramaEvents();
}
+
+ this.element.trigger('AddFotoramaVideoEvents:loaded');
},
/**
diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php
index bf2c6bbc57d95..7741d3b0f7657 100644
--- a/app/code/Magento/Quote/Model/Quote.php
+++ b/app/code/Magento/Quote/Model/Quote.php
@@ -1057,7 +1057,7 @@ public function addCustomerAddress(\Magento\Customer\Api\Data\AddressInterface $
public function updateCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer)
{
$quoteCustomer = $this->getCustomer();
- $this->dataObjectHelper->mergeDataObjects(get_class($quoteCustomer), $quoteCustomer, $customer);
+ $this->dataObjectHelper->mergeDataObjects(CustomerInterface::class, $quoteCustomer, $customer);
$this->setCustomer($quoteCustomer);
return $this;
}
diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php
index 72c1942159016..9bdcb083808ad 100644
--- a/app/code/Magento/Quote/Model/QuoteManagement.php
+++ b/app/code/Magento/Quote/Model/QuoteManagement.php
@@ -412,17 +412,24 @@ public function submit(QuoteEntity $quote, $orderData = [])
*/
protected function resolveItems(QuoteEntity $quote)
{
- $quoteItems = [];
- foreach ($quote->getAllItems() as $quoteItem) {
- /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $quoteItem */
- $quoteItems[$quoteItem->getId()] = $quoteItem;
- }
$orderItems = [];
- foreach ($quoteItems as $quoteItem) {
- $parentItem = (isset($orderItems[$quoteItem->getParentItemId()])) ?
- $orderItems[$quoteItem->getParentItemId()] : null;
- $orderItems[$quoteItem->getId()] =
- $this->quoteItemToOrderItem->convert($quoteItem, ['parent_item' => $parentItem]);
+ foreach ($quote->getAllItems() as $quoteItem) {
+ $itemId = $quoteItem->getId();
+
+ if (!empty($orderItems[$itemId])) {
+ continue;
+ }
+
+ $parentItemId = $quoteItem->getParentItemId();
+ /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $parentItem */
+ if ($parentItemId && !isset($orderItems[$parentItemId])) {
+ $orderItems[$parentItemId] = $this->quoteItemToOrderItem->convert(
+ $quoteItem->getParentItem(),
+ ['parent_item' => null]
+ );
+ }
+ $parentItem = isset($orderItems[$parentItemId]) ? $orderItems[$parentItemId] : null;
+ $orderItems[$itemId] = $this->quoteItemToOrderItem->convert($quoteItem, ['parent_item' => $parentItem]);
}
return array_values($orderItems);
}
@@ -468,6 +475,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
'email' => $quote->getCustomerEmail()
]
);
+ $shippingAddress->setData('quote_address_id', $quote->getShippingAddress()->getId());
$addresses[] = $shippingAddress;
$order->setShippingAddress($shippingAddress);
$order->setShippingMethod($quote->getShippingAddress()->getShippingMethod());
@@ -479,6 +487,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
'email' => $quote->getCustomerEmail()
]
);
+ $billingAddress->setData('quote_address_id', $quote->getBillingAddress()->getId());
$addresses[] = $billingAddress;
$order->setBillingAddress($billingAddress);
$order->setAddresses($addresses);
diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
index 6a04c34a23ec4..5161c3a7f01f2 100644
--- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
+++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
@@ -147,6 +147,21 @@ public function resetJoinQuotes($quotesTableName, $productId = null)
return $this;
}
+ /**
+ * Join product entities to select existing products items only
+ *
+ * @return void
+ */
+ protected function _beforeLoad()
+ {
+ parent::_beforeLoad();
+ $this->join(
+ ['cpe' => $this->getResource()->getTable('catalog_product_entity')],
+ "cpe.entity_id = main_table.product_id",
+ []
+ );
+ }
+
/**
* After load processing
*
diff --git a/app/code/Magento/Quote/Setup/UpgradeSchema.php b/app/code/Magento/Quote/Setup/UpgradeSchema.php
index e4912892dbe17..1bb20a669bdf2 100644
--- a/app/code/Magento/Quote/Setup/UpgradeSchema.php
+++ b/app/code/Magento/Quote/Setup/UpgradeSchema.php
@@ -48,17 +48,17 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con
}
//drop foreign key for single DB case
if (version_compare($context->getVersion(), '2.0.3', '<')
- && $setup->tableExists($setup->getTable('quote_item'))
+ && $setup->tableExists($setup->getTable('quote_item', self::$connectionName))
) {
- $setup->getConnection()->dropForeignKey(
- $setup->getTable('quote_item'),
+ $setup->getConnection(self::$connectionName)->dropForeignKey(
+ $setup->getTable('quote_item', self::$connectionName),
$setup->getFkName('quote_item', 'product_id', 'catalog_product_entity', 'entity_id')
);
}
if (version_compare($context->getVersion(), '2.0.5', '<')) {
- $connection = $setup->getConnection();
+ $connection = $setup->getConnection(self::$connectionName);
$connection->modifyColumn(
- $setup->getTable('quote_address'),
+ $setup->getTable('quote_address', self::$connectionName),
'shipping_method',
[
'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
index d01ae7304bdc6..e25b770b7a81e 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
@@ -34,6 +34,7 @@
* Test class for sales quote address model
*
* @see \Magento\Quote\Model\Quote\Address
+ * @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class AddressTest extends \PHPUnit\Framework\TestCase
@@ -48,6 +49,11 @@ class AddressTest extends \PHPUnit\Framework\TestCase
*/
private $quote;
+ /**
+ * @var \Magento\Quote\Model\Quote\Address\CustomAttributeListInterface | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $attributeList;
+
/**
* @var \Magento\Framework\App\Config | \PHPUnit_Framework_MockObject_MockObject
*/
@@ -166,9 +172,13 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
+ $this->attributeList = $this->createMock(\Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::class);
+ $this->attributeList->method('getAttributes')->willReturn([]);
+
$this->address = $objectManager->getObject(
\Magento\Quote\Model\Quote\Address::class,
[
+ 'attributeList' => $this->attributeList,
'scopeConfig' => $this->scopeConfig,
'serializer' => $this->serializer,
'storeManager' => $this->storeManager,
diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
index 8316885e9b45c..145a18fb34ca3 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
@@ -7,8 +7,8 @@
namespace Magento\Quote\Test\Unit\Model;
use Magento\Framework\Exception\NoSuchEntityException;
-
use Magento\Quote\Model\CustomerManagement;
+use Magento\Sales\Api\Data\OrderAddressInterface;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -540,12 +540,12 @@ public function testSubmit()
$shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
$payment = $this->createMock(\Magento\Quote\Model\Quote\Payment::class);
$baseOrder = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class);
- $convertedBillingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class);
- $convertedShippingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class);
+ $convertedBilling = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']);
+ $convertedShipping = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']);
$convertedPayment = $this->createMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class);
$convertedQuoteItem = $this->createMock(\Magento\Sales\Api\Data\OrderItemInterface::class);
- $addresses = [$convertedShippingAddress, $convertedBillingAddress];
+ $addresses = [$convertedShipping, $convertedBilling];
$quoteItems = [$quoteItem];
$convertedItems = [$convertedQuoteItem];
@@ -574,7 +574,7 @@ public function testSubmit()
'email' => 'customer@example.com'
]
)
- ->willReturn($convertedShippingAddress);
+ ->willReturn($convertedShipping);
$this->quoteAddressToOrderAddress->expects($this->at(1))
->method('convert')
->with(
@@ -584,22 +584,27 @@ public function testSubmit()
'email' => 'customer@example.com'
]
)
- ->willReturn($convertedBillingAddress);
+ ->willReturn($convertedBilling);
+
+ $billingAddress->expects($this->once())->method('getId')->willReturn(4);
+ $convertedBilling->expects($this->once())->method('setData')->with('quote_address_id', 4);
$this->quoteItemToOrderItem->expects($this->once())->method('convert')
->with($quoteItem, ['parent_item' => null])
->willReturn($convertedQuoteItem);
$this->quotePaymentToOrderPayment->expects($this->once())->method('convert')->with($payment)
->willReturn($convertedPayment);
$shippingAddress->expects($this->once())->method('getShippingMethod')->willReturn('free');
+ $shippingAddress->expects($this->once())->method('getId')->willReturn(5);
+ $convertedShipping->expects($this->once())->method('setData')->with('quote_address_id', 5);
$order = $this->prepareOrderFactory(
$baseOrder,
- $convertedBillingAddress,
+ $convertedBilling,
$addresses,
$convertedPayment,
$convertedItems,
$quoteId,
- $convertedShippingAddress
+ $convertedShipping
);
$this->orderManagement->expects($this->once())
@@ -973,9 +978,6 @@ protected function setPropertyValue(&$object, $property, $value)
return $object;
}
- /**
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
public function testSubmitForCustomer()
{
$orderData = [];
@@ -988,16 +990,12 @@ public function testSubmitForCustomer()
$shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
$payment = $this->createMock(\Magento\Quote\Model\Quote\Payment::class);
$baseOrder = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class);
- $convertedBillingAddress = $this->createMock(
- \Magento\Sales\Api\Data\OrderAddressInterface::class
- );
- $convertedShippingAddress = $this->createMock(
- \Magento\Sales\Api\Data\OrderAddressInterface::class
- );
+ $convertedBilling = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']);
+ $convertedShipping = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']);
$convertedPayment = $this->createMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class);
$convertedQuoteItem = $this->createMock(\Magento\Sales\Api\Data\OrderItemInterface::class);
- $addresses = [$convertedShippingAddress, $convertedBillingAddress];
+ $addresses = [$convertedShipping, $convertedBilling];
$quoteItems = [$quoteItem];
$convertedItems = [$convertedQuoteItem];
@@ -1026,7 +1024,7 @@ public function testSubmitForCustomer()
'email' => 'customer@example.com'
]
)
- ->willReturn($convertedShippingAddress);
+ ->willReturn($convertedShipping);
$this->quoteAddressToOrderAddress->expects($this->at(1))
->method('convert')
->with(
@@ -1036,22 +1034,24 @@ public function testSubmitForCustomer()
'email' => 'customer@example.com'
]
)
- ->willReturn($convertedBillingAddress);
+ ->willReturn($convertedBilling);
$this->quoteItemToOrderItem->expects($this->once())->method('convert')
->with($quoteItem, ['parent_item' => null])
->willReturn($convertedQuoteItem);
$this->quotePaymentToOrderPayment->expects($this->once())->method('convert')->with($payment)
->willReturn($convertedPayment);
$shippingAddress->expects($this->once())->method('getShippingMethod')->willReturn('free');
+ $shippingAddress->expects($this->once())->method('getId')->willReturn(5);
+ $convertedShipping->expects($this->once())->method('setData')->with('quote_address_id', 5);
$order = $this->prepareOrderFactory(
$baseOrder,
- $convertedBillingAddress,
+ $convertedBilling,
$addresses,
$convertedPayment,
$convertedItems,
$quoteId,
- $convertedShippingAddress
+ $convertedShipping
);
$customerAddressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class)
->getMockForAbstractClass();
@@ -1060,6 +1060,8 @@ public function testSubmitForCustomer()
$quote->expects($this->any())->method('addCustomerAddress')->with($customerAddressMock);
$billingAddress->expects($this->once())->method('getCustomerId')->willReturn(2);
$billingAddress->expects($this->once())->method('getSaveInAddressBook')->willReturn(false);
+ $billingAddress->expects($this->once())->method('getId')->willReturn(4);
+ $convertedBilling->expects($this->once())->method('setData')->with('quote_address_id', 4);
$this->orderManagement->expects($this->once())
->method('place')
->with($order)
@@ -1073,4 +1075,25 @@ public function testSubmitForCustomer()
$this->quoteRepositoryMock->expects($this->once())->method('save')->with($quote);
$this->assertEquals($order, $this->model->submit($quote, $orderData));
}
+
+ /**
+ * Get mock for abstract class with methods.
+ *
+ * @param string $className
+ * @param array $methods
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createPartialMockForAbstractClass($className, $methods = [])
+ {
+ return $this->getMockForAbstractClass(
+ $className,
+ [],
+ '',
+ true,
+ true,
+ true,
+ $methods
+ );
+ }
}
diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json
index 5d4c5f35a3a24..a892697802e63 100644
--- a/app/code/Magento/Quote/composer.json
+++ b/app/code/Magento/Quote/composer.json
@@ -23,7 +23,7 @@
"magento/module-webapi": "100.2.*"
},
"type": "magento2-module",
- "version": "101.0.0",
+ "version": "101.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/QuoteAnalytics/LICENSE.txt b/app/code/Magento/QuoteAnalytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/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/QuoteAnalytics/LICENSE_AFL.txt b/app/code/Magento/QuoteAnalytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/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/QuoteAnalytics/README.md b/app/code/Magento/QuoteAnalytics/README.md
new file mode 100644
index 0000000000000..d4adcc9313229
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/README.md
@@ -0,0 +1,3 @@
+# Magento_QuoteAnalytics
+
+The Magento_QuoteAnalytics module configures data definitions for a data collection related to the Quote module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html).
diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json
new file mode 100644
index 0000000000000..abbb0ce7a042b
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "magento/module-quote-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "100.2.*",
+ "magento/module-quote": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\QuoteAnalytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/QuoteAnalytics/etc/analytics.xml b/app/code/Magento/QuoteAnalytics/etc/analytics.xml
new file mode 100644
index 0000000000000..cc4dfb6364904
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/etc/analytics.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ quotes
+
+
+
+
+
diff --git a/app/code/Magento/QuoteAnalytics/etc/module.xml b/app/code/Magento/QuoteAnalytics/etc/module.xml
new file mode 100644
index 0000000000000..d72e36b748748
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/QuoteAnalytics/etc/reports.xml b/app/code/Magento/QuoteAnalytics/etc/reports.xml
new file mode 100644
index 0000000000000..f57012df23389
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/etc/reports.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/QuoteAnalytics/registration.php b/app/code/Magento/QuoteAnalytics/registration.php
new file mode 100644
index 0000000000000..19718c3cf2adf
--- /dev/null
+++ b/app/code/Magento/QuoteAnalytics/registration.php
@@ -0,0 +1,11 @@
+productMetadata = $productMetadata;
+ $this->notificationLogger = $notificationLogger;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Log information about the last shown advertisement
+ *
+ * @return \Magento\Framework\Controller\ResultInterface
+ */
+ public function execute()
+ {
+ try {
+ $responseContent = [
+ 'success' => $this->notificationLogger->log(
+ $this->_auth->getUser()->getId(),
+ $this->productMetadata->getVersion()
+ ),
+ 'error_message' => ''
+ ];
+ } catch (LocalizedException $e) {
+ $this->logger->error($e->getMessage());
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => $e->getMessage()
+ ];
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage());
+ $responseContent = [
+ 'success' => false,
+ 'error_message' => __('It is impossible to log user action')
+ ];
+ }
+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+ return $resultJson->setData($responseContent);
+ }
+
+ protected function _isAllowed()
+ {
+ return parent::_isAllowed();
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/LICENSE.txt b/app/code/Magento/ReleaseNotification/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/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/ReleaseNotification/LICENSE_AFL.txt b/app/code/Magento/ReleaseNotification/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/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/ReleaseNotification/Model/Condition/CanViewNotification.php b/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php
new file mode 100644
index 0000000000000..07e26bb1a4d8d
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php
@@ -0,0 +1,106 @@
+viewerLogger = $viewerLogger;
+ $this->session = $session;
+ $this->productMetadata = $productMetadata;
+ $this->cacheStorage = $cacheStorage;
+ }
+
+ /**
+ * Validate if notification popup can be shown and set the notification flag
+ *
+ * @inheritdoc
+ */
+ public function isVisible(array $arguments)
+ {
+ $userId = $this->session->getUser()->getId();
+ $cacheKey = self::$cachePrefix . $userId;
+ $value = $this->cacheStorage->load($cacheKey);
+ if ($value === false) {
+ $value = version_compare(
+ $this->viewerLogger->get($userId)->getLastViewVersion(),
+ $this->productMetadata->getVersion(),
+ '<'
+ );
+ $this->cacheStorage->save(false, $cacheKey);
+ }
+ return (bool)$value;
+ }
+
+ /**
+ * Get condition name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return self::$conditionName;
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/Model/ResourceModel/Viewer/Logger.php b/app/code/Magento/ReleaseNotification/Model/ResourceModel/Viewer/Logger.php
new file mode 100644
index 0000000000000..967ccabcdb49c
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Model/ResourceModel/Viewer/Logger.php
@@ -0,0 +1,103 @@
+resource = $resource;
+ $this->logFactory = $logFactory;
+ }
+
+ /**
+ * Save (insert new or update existing) log.
+ *
+ * @param int $viewerId
+ * @param string $lastViewVersion
+ * @return bool
+ */
+ public function log(int $viewerId, string $lastViewVersion) : bool
+ {
+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
+ $connection = $this->resource->getConnection(ResourceConnection::DEFAULT_CONNECTION);
+ $connection->insertOnDuplicate(
+ $this->resource->getTableName(self::LOG_TABLE_NAME),
+ [
+ 'viewer_id' => $viewerId,
+ 'last_view_version' => $lastViewVersion
+ ],
+ [
+ 'last_view_version'
+ ]
+ );
+ return true;
+ }
+
+ /**
+ * Get log by viewer Id.
+ *
+ * @param int $viewerId
+ * @return Log
+ */
+ public function get(int $viewerId) : Log
+ {
+ return $this->logFactory->create(['data' => $this->loadLogData($viewerId)]);
+ }
+
+ /**
+ * Load release notification viewer log data by viewer id
+ *
+ * @param int $viewerId
+ * @return array
+ */
+ private function loadLogData(int $viewerId) : array
+ {
+ $connection = $this->resource->getConnection();
+ $select = $connection->select()
+ ->from($this->resource->getTableName(self::LOG_TABLE_NAME))
+ ->where('viewer_id = ?', $viewerId);
+
+ $data = $connection->fetchRow($select);
+ if (!$data) {
+ $data = [];
+ }
+ return $data;
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/Model/Viewer/Log.php b/app/code/Magento/ReleaseNotification/Model/Viewer/Log.php
new file mode 100644
index 0000000000000..27100b62fa798
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Model/Viewer/Log.php
@@ -0,0 +1,46 @@
+getData('id');
+ }
+
+ /**
+ * Get viewer id
+ *
+ * @return int
+ */
+ public function getViewerId()
+ {
+ return $this->getData('viewer_id');
+ }
+
+ /**
+ * Get last viewed product version
+ *
+ * @return string
+ */
+ public function getLastViewVersion()
+ {
+ return $this->getData('last_view_version');
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md
new file mode 100644
index 0000000000000..bb0a6e7f4cf80
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/README.md
@@ -0,0 +1,11 @@
+ # Magento_ReleaseNotification Module
+
+The **Release Notification Module** serves to provide a notification delivery platform for displaying new features of a Magento installation or upgrade as well as any other required release notifications.
+
+## Purpose and Content
+
+* Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release
+* Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version
+* The modal is enabled with pagination functionality to allow for easy navigation between each modal page
+* Each modal page includes detailed information about a highlighted feature of the Magento release or other notification
+* Release Notification modal content is determined and provided by Magento Marketing
diff --git a/app/code/Magento/ReleaseNotification/Setup/InstallSchema.php b/app/code/Magento/ReleaseNotification/Setup/InstallSchema.php
new file mode 100644
index 0000000000000..a6a0f51befa3f
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Setup/InstallSchema.php
@@ -0,0 +1,69 @@
+startSetup();
+
+ /**
+ * Create table 'release_notification_viewer_log'
+ */
+ $table = $setup->getConnection()->newTable(
+ $setup->getTable('release_notification_viewer_log')
+ )->addColumn(
+ 'id',
+ \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
+ null,
+ ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
+ 'Log ID'
+ )->addColumn(
+ 'viewer_id',
+ \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Viewer admin user ID'
+ )->addColumn(
+ 'last_view_version',
+ \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
+ 16,
+ ['nullable' => false],
+ 'Viewer last view on product version'
+ )->addIndex(
+ $setup->getIdxName(
+ 'release_notification_viewer_log',
+ ['viewer_id'],
+ \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE
+ ),
+ ['viewer_id'],
+ ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE]
+ )->addForeignKey(
+ $setup->getFkName('release_notification_viewer_log', 'viewer_id', 'admin_user', 'user_id'),
+ 'viewer_id',
+ $setup->getTable('admin_user'),
+ 'user_id',
+ Table::ACTION_CASCADE
+ )->setComment(
+ 'Release Notification Viewer Log Table'
+ );
+ $setup->getConnection()->createTable($table);
+
+ $setup->endSetup();
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Controller/Notification/MarkUserNotifiedTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Controller/Notification/MarkUserNotifiedTest.php
new file mode 100644
index 0000000000000..894368cbcba01
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Test/Unit/Controller/Notification/MarkUserNotifiedTest.php
@@ -0,0 +1,189 @@
+storageMock = $this->getMockBuilder(StorageInterface::class)
+ ->setMethods(['getId'])
+ ->getMockForAbstractClass();
+ $this->authMock = $this->getMockBuilder(Auth::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $contextMock->expects($this->once())
+ ->method('getAuth')
+ ->willReturn($this->authMock);
+ $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class)
+ ->getMockForAbstractClass();
+ $this->notificationLoggerMock = $this->getMockBuilder(NotificationLogger::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->getMock();
+ $resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultMock = $this->getMockBuilder(Json::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $resultFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_JSON)
+ ->willReturn($this->resultMock);
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->action = $objectManagerHelper->getObject(
+ MarkUserNotified::class,
+ [
+ 'resultFactory' => $resultFactoryMock,
+ 'productMetadata' => $this->productMetadataMock,
+ 'notificationLogger' => $this->notificationLoggerMock,
+ 'context' => $contextMock,
+ 'logger' => $this->loggerMock
+ ]
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->authMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->storageMock);
+ $this->storageMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $this->productMetadataMock->expects($this->once())
+ ->method('getVersion')
+ ->willReturn('999.999.999-alpha');
+ $this->notificationLoggerMock->expects($this->once())
+ ->method('log')
+ ->with(1, '999.999.999-alpha')
+ ->willReturn(true);
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => true,
+ 'error_message' => ''
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+
+ public function testExecuteFailedWithLocalizedException()
+ {
+ $this->authMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->storageMock);
+ $this->storageMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $this->productMetadataMock->expects($this->once())
+ ->method('getVersion')
+ ->willReturn('999.999.999-alpha');
+ $this->notificationLoggerMock->expects($this->once())
+ ->method('log')
+ ->willThrowException(new LocalizedException(__('Error message')));
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => false,
+ 'error_message' => 'Error message'
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+
+ public function testExecuteFailedWithException()
+ {
+ $this->authMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->storageMock);
+ $this->storageMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $this->productMetadataMock->expects($this->once())
+ ->method('getVersion')
+ ->willReturn('999.999.999-alpha');
+ $this->notificationLoggerMock->expects($this->once())
+ ->method('log')
+ ->willThrowException(new \Exception('Any message'));
+ $this->resultMock->expects($this->once())
+ ->method('setData')
+ ->with(
+ [
+ 'success' => false,
+ 'error_message' => __('It is impossible to log user action')
+ ],
+ false,
+ []
+ )->willReturnSelf();
+ $this->assertEquals($this->resultMock, $this->action->execute());
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php
new file mode 100644
index 0000000000000..3ec00697507c1
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php
@@ -0,0 +1,128 @@
+cacheStorageMock = $this->getMockBuilder(CacheInterface::class)
+ ->getMockForAbstractClass();
+ $this->logMock = $this->getMockBuilder(Log::class)
+ ->getMock();
+ $this->sessionMock = $this->getMockBuilder(Session::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getUser', 'getId'])
+ ->getMock();
+ $this->viewerLoggerMock = $this->getMockBuilder(Logger::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManager = new ObjectManager($this);
+ $this->canViewNotification = $objectManager->getObject(
+ CanViewNotification::class,
+ [
+ 'viewerLogger' => $this->viewerLoggerMock,
+ 'session' => $this->sessionMock,
+ 'productMetadata' => $this->productMetadataMock,
+ 'cacheStorage' => $this->cacheStorageMock,
+ ]
+ );
+ }
+
+ public function testIsVisibleLoadDataFromCache()
+ {
+ $this->sessionMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->sessionMock);
+ $this->sessionMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $this->cacheStorageMock->expects($this->once())
+ ->method('load')
+ ->with('release-notification-popup-1')
+ ->willReturn("0");
+ $this->assertEquals(false, $this->canViewNotification->isVisible([]));
+ }
+
+ /**
+ * @param bool $expected
+ * @param string $version
+ * @param string|null $lastViewVersion
+ * @dataProvider isVisibleProvider
+ */
+ public function testIsVisible($expected, $version, $lastViewVersion)
+ {
+ $this->cacheStorageMock->expects($this->once())
+ ->method('load')
+ ->with('release-notification-popup-1')
+ ->willReturn(false);
+ $this->sessionMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->sessionMock);
+ $this->sessionMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $this->productMetadataMock->expects($this->once())
+ ->method('getVersion')
+ ->willReturn($version);
+ $this->logMock->expects($this->once())
+ ->method('getLastViewVersion')
+ ->willReturn($lastViewVersion);
+ $this->viewerLoggerMock->expects($this->once())
+ ->method('get')
+ ->with(1)
+ ->willReturn($this->logMock);
+ $this->cacheStorageMock->expects($this->once())
+ ->method('save')
+ ->with(false, 'release-notification-popup-1');
+ $this->assertEquals($expected, $this->canViewNotification->isVisible([]));
+ }
+
+ public function isVisibleProvider()
+ {
+ return [
+ [false, '2.2.1-dev', '999.999.999-alpha'],
+ [true, '2.2.1-dev', '2.0.0'],
+ [true, '2.2.1-dev', null],
+ [false, '2.2.1-dev', '2.2.1'],
+ [true, '2.2.1-dev', '2.2.0'],
+ [true, '2.3.0', '2.2.0'],
+ [false, '2.2.2', '2.2.2'],
+ ];
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/Ui/DataProvider/DataProvider.php b/app/code/Magento/ReleaseNotification/Ui/DataProvider/DataProvider.php
new file mode 100644
index 0000000000000..48f01b3b058e2
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/Ui/DataProvider/DataProvider.php
@@ -0,0 +1,198 @@
+name = $name;
+ $this->searchResult = $searchResult;
+ $this->searchCriteria = $searchCriteria;
+ $this->collection = $collection;
+ $this->data = $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfigData()
+ {
+ return isset($this->data['config']) ? $this->data['config'] : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfigData($config)
+ {
+ $this->data['config'] = $config;
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMeta()
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldMetaInfo($fieldSetName, $fieldName)
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldSetMetaInfo($fieldSetName)
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function getFieldsMetaInfo($fieldSetName)
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPrimaryFieldName()
+ {
+ return 'release_notification';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequestFieldName()
+ {
+ return 'release_notification';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData()
+ {
+ return $this->collection->toArray();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function addFilter(\Magento\Framework\Api\Filter $filter)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function addOrder($field, $direction)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function setLimit($offset, $size)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchCriteria()
+ {
+ return $this->searchCriteria;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchResult()
+ {
+ return $this->searchResult;
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/composer.json b/app/code/Magento/ReleaseNotification/composer.json
new file mode 100644
index 0000000000000..b338420a2c143
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "magento/module-release-notification",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/module-user": "101.0.*",
+ "magento/module-backend": "100.2.*",
+ "magento/framework": "101.0.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\ReleaseNotification\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/ReleaseNotification/etc/adminhtml/routes.xml b/app/code/Magento/ReleaseNotification/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..4b1ddc69ce3bd
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ReleaseNotification/etc/module.xml b/app/code/Magento/ReleaseNotification/etc/module.xml
new file mode 100644
index 0000000000000..134d82e4f5776
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv
new file mode 100644
index 0000000000000..0f4a1f87b8b4e
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv
@@ -0,0 +1,112 @@
+"Next >","Next >"
+"< Back","< Back"
+"Done","Done"
+"What's new with Magento 2.2.2","What's new with Magento 2.2.2"
+"Magento 2.2.2 offers advanced new features, including:
+
+
+
Advanced Reporting
+
Gain valuable insights through a dynamic suite of product, order, and customer reports,
+ powered by Magento Business Intelligence.
+
+
+
Instant Purchase
+
Simplify ordering and boost conversion rates by allowing your customers to use stored
+ payment and shipping information to skip tedious checkout steps.
+
+
+
Email Marketing Automation
+
Send smarter, faster email campaigns with marketing automation from dotmailer, powered by
+ your Magento store's live data.
+
+ Release notes and additional details can be found at
+ Magento DevDocs .
+
","Magento 2.2.2 offers advanced new features, including:
+
+
+
Advanced Reporting
+
Gain valuable insights through a dynamic suite of product, order, and customer reports,
+ powered by Magento Business Intelligence.
+
+
+
Instant Purchase
+
Simplify ordering and boost conversion rates by allowing your customers to use stored
+ payment and shipping information to skip tedious checkout steps.
+
+
+
Email Marketing Automation
+
Send smarter, faster email campaigns with marketing automation from dotmailer, powered by
+ your Magento store's live data.
+
+ Release notes and additional details can be found at
+ Magento DevDocs .
+
"
+"Advanced Reporting","Advanced Reporting"
+"Advanced Reporting
+ provides you with a dynamic suite of reports with rich insights about the health of your
+ business.
As part of the Advanced Reporting service, we may also use your customer
+ data for such purposes as benchmarking, improving our products and services, and providing you
+ with new and improved analytics.
By using Magento 2.2.2, you agree to the Advanced
+ Reporting Privacy Policy and
+ Terms
+ of Service . You may opt out at any time from the Stores Configuration page.
+ ","Advanced Reporting
+ provides you with a dynamic suite of reports with rich insights about the health of your
+ business.
As part of the Advanced Reporting service, we may also use your customer
+ data for such purposes as benchmarking, improving our products and services, and providing you
+ with new and improved analytics.
By using Magento 2.2.2, you agree to the Advanced
+ Reporting Privacy Policy and
+ Terms
+ of Service . You may opt out at any time from the Stores Configuration page.
"
+"Instant Purchase","Instant Purchase"
+"Now you can deliver an Amazon-like experience with a new, streamlined checkout option.
+ Logged-in customers can use previously-stored payment credentials and shipping information
+ to skip steps, making the process faster and easier, especially for mobile shoppers. Key
+ features include:
+
+
+ Configurable “Instant Purchase” button to place orders
+ Support for all payment solutions using Braintree Vault, including Braintree Credit
+ Card, Braintree PayPal, and PayPal Payflow Pro.
+ Shipping to the customer’s default address using the lowest cost, available shipping
+ method
+ Ability for developers to customize the Instant Purchase business logic to meet
+ merchant needs
+ ","Now you can deliver an Amazon-like experience with a new, streamlined checkout option.
+ Logged-in customers can use previously-stored payment credentials and shipping information
+ to skip steps, making the process faster and easier, especially for mobile shoppers. Key
+ features include:
+
+
+ Configurable “Instant Purchase” button to place orders
+ Support for all payment solutions using Braintree Vault, including Braintree Credit
+ Card, Braintree PayPal, and PayPal Payflow Pro.
+ Shipping to the customer’s default address using the lowest cost, available shipping
+ method
+ Ability for developers to customize the Instant Purchase business logic to meet
+ merchant needs
+ "
+"Email Marketing Automation","Email Marketing Automation"
+"Unlock an unparalleled level of insight and control of your eCommerce marketing with
+ dotmailer Email Marketing Automation. Included with Magento 2.2.2 for easy set-up, dotmailer
+ ensures every customer’s journey is captured, segmented, and personalized, enabling you to
+ deliver customer-centric campaigns that beat your results over and over again. Benefits include:
+
+
+ No obligation 14-day trial.
+ Automation campaigns using your live Magento store data that drive revenue,
+ including abandoned cart, abandoned browse, product replenishment, and many more
+ Built-in solution for transactional emails.
+ Telephone support and advice from marketing experts included.
+ ","Unlock an unparalleled level of insight and control of your eCommerce marketing with
+ dotmailer Email Marketing Automation. Included with Magento 2.2.2 for easy set-up, dotmailer
+ ensures every customer’s journey is captured, segmented, and personalized, enabling you to
+ deliver customer-centric campaigns that beat your results over and over again. Benefits include:
+
+
+ No obligation 14-day trial.
+ Automation campaigns using your live Magento store data that drive revenue,
+ including abandoned cart, abandoned browse, product replenishment, and many more
+ Built-in solution for transactional emails.
+ Telephone support and advice from marketing experts included.
+ "
diff --git a/app/code/Magento/ReleaseNotification/registration.php b/app/code/Magento/ReleaseNotification/registration.php
new file mode 100644
index 0000000000000..c5bce27f20387
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml
new file mode 100644
index 0000000000000..1ddfde5cafb9c
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml
@@ -0,0 +1,377 @@
+
+
+
diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/web/js/modal/component.js b/app/code/Magento/ReleaseNotification/view/adminhtml/web/js/modal/component.js
new file mode 100644
index 0000000000000..b74ef2af1a04d
--- /dev/null
+++ b/app/code/Magento/ReleaseNotification/view/adminhtml/web/js/modal/component.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'Magento_Ui/js/modal/modal-component',
+ 'Magento_Ui/js/modal/alert',
+ 'mage/translate'
+], function ($, Modal, alert, $t) {
+ 'use strict';
+
+ return Modal.extend({
+ defaults: {
+ imports: {
+ logAction: '${ $.provider }:data.logAction'
+ }
+ },
+
+ /**
+ * Error handler.
+ *
+ * @param {Object} xhr - request result.
+ */
+ onError: function (xhr) {
+ if (xhr.statusText === 'abort') {
+ return;
+ }
+
+ alert({
+ content: xhr.message || $t('An error occurred while logging process.')
+ });
+ },
+
+ /**
+ * Log release notes show
+ */
+ logReleaseNotesShow: function () {
+ var self = this,
+ data = {
+ 'form_key': window.FORM_KEY
+ };
+
+ $.ajax({
+ type: 'POST',
+ url: this.logAction,
+ data: data,
+ showLoader: true
+ }).done(function (xhr) {
+ if (xhr.error) {
+ self.onError(xhr);
+ }
+ }).fail(this.onError);
+ },
+
+ /**
+ * Close release notes
+ */
+ closeReleaseNotes: function () {
+ this.logReleaseNotesShow();
+ this.closeModal();
+ }
+ });
+});
diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php
index 48a87bf77cf94..158455db26455 100644
--- a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php
+++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php
@@ -363,12 +363,11 @@ public function setStoreIds($storeIds)
public function getCurrentCurrencyCode()
{
if ($this->_currentCurrencyCode === null) {
- $this->_currentCurrencyCode = count(
- $this->_storeIds
- ) > 0 ? $this->_storeManager->getStore(
- array_shift($this->_storeIds)
- )->getBaseCurrencyCode() : $this->_storeManager->getStore()->getBaseCurrencyCode();
+ $this->_currentCurrencyCode = count($this->_storeIds) > 0
+ ? $this->_storeManager->getStore(array_shift($this->_storeIds))->getCurrentCurrencyCode()
+ : $this->_storeManager->getStore()->getBaseCurrencyCode();
}
+
return $this->_currentCurrencyCode;
}
diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php
index cc21b8dc98395..337c87f6da03d 100644
--- a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php
@@ -298,7 +298,7 @@ public function setOrder($attribute, $dir = self::SORT_ORDER_DESC)
}
/**
- * Add views count
+ * Add views count.
*
* @param string $from
* @param string $to
@@ -322,10 +322,7 @@ public function addViewsCount($from = '', $to = '')
['views' => 'COUNT(report_table_views.event_id)']
)->join(
['e' => $this->getProductEntityTableName()],
- $this->getConnection()->quoteInto(
- 'e.entity_id = report_table_views.object_id AND e.attribute_set_id = ?',
- $this->getProductAttributeSetId()
- )
+ 'e.entity_id = report_table_views.object_id'
)->where(
'report_table_views.event_type_id = ?',
$productViewEvent
@@ -341,6 +338,7 @@ public function addViewsCount($from = '', $to = '')
if ($from != '' && $to != '') {
$this->getSelect()->where('logged_at >= ?', $from)->where('logged_at <= ?', $to);
}
+
return $this;
}
diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php
index 6e891c481aebe..f3f14ef8c3543 100644
--- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php
+++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php
@@ -192,7 +192,7 @@ protected function getProductData(array $productIds)
. ' AND product_name.attribute_id = ' . $productAttrNameId
. ' AND product_name.store_id = ' . \Magento\Store\Model\Store::DEFAULT_STORE_ID,
['name' => 'product_name.value']
- )->joinInner(
+ )->joinLeft(
['product_price' => $productAttrPrice->getBackend()->getTable()],
"product_price.{$linkField} = main_table.{$linkField}"
." AND product_price.attribute_id = {$productAttrPriceId}",
diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/AbstractGridTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/AbstractGridTest.php
new file mode 100644
index 0000000000000..dc16928861b1c
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/AbstractGridTest.php
@@ -0,0 +1,91 @@
+storeManagerMock = $this->getMockForAbstractClass(
+ \Magento\Store\Model\StoreManagerInterface::class,
+ [],
+ '',
+ true,
+ true,
+ true,
+ ['getStore']
+ );
+
+ $this->model = $objectManager->getObject(
+ \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid::class,
+ ['_storeManager' => $this->storeManagerMock]
+ );
+ }
+
+ /**
+ * @param $storeIds
+ *
+ * @dataProvider getCurrentCurrencyCodeDataProvider
+ */
+ public function testGetCurrentCurrencyCode($storeIds)
+ {
+ $storeMock = $this->getMockForAbstractClass(
+ \Magento\Store\Api\Data\StoreInterface::class,
+ [],
+ '',
+ true,
+ true,
+ true,
+ ['getBaseCurrencyCode', 'getCurrentCurrencyCode']
+ );
+
+ $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock);
+
+ $this->model->setStoreIds($storeIds);
+
+ if ($storeIds) {
+ $storeMock->expects($this->once())->method('getCurrentCurrencyCode')->willReturn('EUR');
+ $expectedCurrencyCode = 'EUR';
+ } else {
+ $storeMock->expects($this->once())->method('getBaseCurrencyCode')->willReturn('USD');
+ $expectedCurrencyCode = 'USD';
+ }
+
+ $currencyCode = $this->model->getCurrentCurrencyCode();
+ $this->assertEquals($expectedCurrencyCode, $currencyCode);
+ }
+
+ /**
+ * DataProvider for testGetCurrentCurrencyCode.
+ *
+ * @return array
+ */
+ public function getCurrentCurrencyCodeDataProvider()
+ {
+ return [
+ [[]],
+ [[2]],
+ ];
+ }
+}
diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
new file mode 100644
index 0000000000000..038d37a990442
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
@@ -0,0 +1,286 @@
+objectManager = new ObjectManager($this);
+ $context = $this->createPartialMock(Context::class, ['getResource', 'getEavConfig']);
+ $entityFactoryMock = $this->createMock(EntityFactory::class);
+ $loggerMock = $this->createMock(LoggerInterface::class);
+ $fetchStrategyMock = $this->createMock(FetchStrategyInterface::class);
+ $eventManagerMock = $this->createMock(ManagerInterface::class);
+ $eavConfigMock = $this->createMock(Config::class);
+ $this->resourceMock = $this->createPartialMock(ResourceConnection::class, ['getTableName', 'getConnection']);
+ $eavEntityFactoryMock = $this->createMock(EavEntityFactory::class);
+ $resourceHelperMock = $this->createMock(Helper::class);
+ $universalFactoryMock = $this->createMock(UniversalFactory::class);
+ $storeManagerMock = $this->createPartialMockForAbstractClass(
+ StoreManagerInterface::class,
+ ['getStore', 'getId']
+ );
+ $moduleManagerMock = $this->createMock(Manager::class);
+ $productFlatStateMock = $this->createMock(State::class);
+ $scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
+ $optionFactoryMock = $this->createMock(OptionFactory::class);
+ $catalogUrlMock = $this->createMock(Url::class);
+ $localeDateMock = $this->createMock(TimezoneInterface::class);
+ $customerSessionMock = $this->createMock(Session::class);
+ $dateTimeMock = $this->createMock(DateTime::class);
+ $groupManagementMock = $this->createMock(GroupManagementInterface::class);
+ $eavConfig = $this->createPartialMock(Config::class, ['getEntityType']);
+ $entityType = $this->createMock(Type::class);
+
+ $eavConfig->expects($this->atLeastOnce())->method('getEntityType')->willReturn($entityType);
+ $context->expects($this->atLeastOnce())->method('getResource')->willReturn($this->resourceMock);
+ $context->expects($this->atLeastOnce())->method('getEavConfig')->willReturn($eavConfig);
+
+ $defaultAttributes = $this->createPartialMock(DefaultAttributes::class, ['_getDefaultAttributes']);
+ $productMock = $this->objectManager->getObject(
+ ResourceProduct::class,
+ ['context' => $context, 'defaultAttributes' => $defaultAttributes]
+ );
+
+ $this->eventTypeFactoryMock = $this->createMock(TypeFactory::class);
+ $productTypeMock = $this->createMock(ProductType::class);
+ $quoteResourceMock = $this->createMock(Collection::class);
+ $this->connectionMock = $this->createPartialMockForAbstractClass(AdapterInterface::class, ['select']);
+ $this->selectMock = $this->createPartialMock(
+ Select::class,
+ [
+ 'reset',
+ 'from',
+ 'join',
+ 'where',
+ 'group',
+ 'order',
+ 'having',
+ ]
+ );
+
+ $storeManagerMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeManagerMock);
+ $storeManagerMock->expects($this->atLeastOnce())->method('getId')->willReturn(1);
+ $universalFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($productMock);
+ $this->resourceMock->expects($this->atLeastOnce())->method('getTableName')->willReturn('test_table');
+ $this->resourceMock->expects($this->atLeastOnce())->method('getConnection')->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock);
+
+ $this->collection = new ProductCollection(
+ $entityFactoryMock,
+ $loggerMock,
+ $fetchStrategyMock,
+ $eventManagerMock,
+ $eavConfigMock,
+ $this->resourceMock,
+ $eavEntityFactoryMock,
+ $resourceHelperMock,
+ $universalFactoryMock,
+ $storeManagerMock,
+ $moduleManagerMock,
+ $productFlatStateMock,
+ $scopeConfigMock,
+ $optionFactoryMock,
+ $catalogUrlMock,
+ $localeDateMock,
+ $customerSessionMock,
+ $dateTimeMock,
+ $groupManagementMock,
+ $productMock,
+ $this->eventTypeFactoryMock,
+ $productTypeMock,
+ $quoteResourceMock,
+ $this->connectionMock
+ );
+ }
+
+ /**
+ * Test addViewsCount behavior.
+ */
+ public function testAddViewsCount()
+ {
+ $context = $this->createPartialMock(
+ \Magento\Framework\Model\ResourceModel\Db\Context::class,
+ ['getResources']
+ );
+ $context->expects($this->atLeastOnce())
+ ->method('getResources')
+ ->willReturn($this->resourceMock);
+ $abstractResourceMock = $this->getMockForAbstractClass(
+ \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class,
+ ['context' => $context],
+ '',
+ true,
+ true,
+ true,
+ [
+ 'getTableName',
+ 'getConnection',
+ 'getMainTable',
+ ]
+ );
+
+ $abstractResourceMock->expects($this->atLeastOnce())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $abstractResourceMock->expects($this->atLeastOnce())
+ ->method('getMainTable')
+ ->willReturn('catalog_product');
+
+ /** @var \Magento\Reports\Model\ResourceModel\Event\Type\Collection $eventTypesCollection */
+ $eventTypesCollection = $this->objectManager->getObject(
+ \Magento\Reports\Model\ResourceModel\Event\Type\Collection::class,
+ ['resource' => $abstractResourceMock]
+ );
+ $eventTypeMock = $this->createPartialMock(
+ \Magento\Reports\Model\Event\Type::class,
+ [
+ 'getEventName',
+ 'getId',
+ 'getCollection',
+ ]
+ );
+
+ $eventTypesCollection->addItem($eventTypeMock);
+
+ $this->eventTypeFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($eventTypeMock);
+ $eventTypeMock->expects($this->atLeastOnce())
+ ->method('getCollection')
+ ->willReturn($eventTypesCollection);
+ $eventTypeMock->expects($this->atLeastOnce())
+ ->method('getEventName')
+ ->willReturn('catalog_product_view');
+ $eventTypeMock->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('reset')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('from')
+ ->with(
+ ['report_table_views' => 'test_table'],
+ ['views' => 'COUNT(report_table_views.event_id)']
+ )->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('join')
+ ->with(
+ ['e' => 'test_table'],
+ 'e.entity_id = report_table_views.object_id'
+ )->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('where')
+ ->with('report_table_views.event_type_id = ?', 1)
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('group')
+ ->with('e.entity_id')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('order')
+ ->with('views DESC')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->atLeastOnce())
+ ->method('having')
+ ->with('COUNT(report_table_views.event_id) > ?', 0)
+ ->willReturn($this->selectMock);
+
+ $this->collection->addViewsCount();
+ }
+
+ /**
+ * Get mock for abstract class with methods.
+ *
+ * @param string $className
+ * @param array $methods
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createPartialMockForAbstractClass($className, $methods)
+ {
+ return $this->getMockForAbstractClass(
+ $className,
+ [],
+ '',
+ true,
+ true,
+ true,
+ $methods
+ );
+ }
+}
diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php
index 051dc3f5f5593..7bb866287d37a 100644
--- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php
+++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php
@@ -124,7 +124,8 @@ public function testLoadWithFilter()
$this->selectMock->expects($this->once())->method('reset')->willReturnSelf();
$this->selectMock->expects($this->once())->method('from')->willReturnSelf();
$this->selectMock->expects($this->once())->method('useStraightJoin')->willReturnSelf();
- $this->selectMock->expects($this->exactly(2))->method('joinInner')->willReturnSelf();
+ $this->selectMock->expects($this->once())->method('joinInner')->willReturnSelf();
+ $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf();
$collection->expects($this->once())->method('getOrdersData')->willReturn([]);
$productAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf();
$priceAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf();
diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json
index 74ed132bc65d2..1058052dd1122 100644
--- a/app/code/Magento/Reports/composer.json
+++ b/app/code/Magento/Reports/composer.json
@@ -22,7 +22,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json
index f3b36e8b3f3fa..47680b454c29f 100644
--- a/app/code/Magento/Review/composer.json
+++ b/app/code/Magento/Review/composer.json
@@ -18,7 +18,7 @@
"magento/module-review-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ReviewAnalytics/LICENSE.txt b/app/code/Magento/ReviewAnalytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/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/ReviewAnalytics/LICENSE_AFL.txt b/app/code/Magento/ReviewAnalytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/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/ReviewAnalytics/README.md b/app/code/Magento/ReviewAnalytics/README.md
new file mode 100644
index 0000000000000..b078083dfb7dc
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/README.md
@@ -0,0 +1,3 @@
+# Magento_ReviewAnalytics module
+
+The Magento_ReviewAnalytics module configures data definitions for a data collection related to the Review module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html).
diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json
new file mode 100644
index 0000000000000..965b6294db16a
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "magento/module-review-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "100.2.*",
+ "magento/module-review": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\ReviewAnalytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/ReviewAnalytics/etc/analytics.xml b/app/code/Magento/ReviewAnalytics/etc/analytics.xml
new file mode 100644
index 0000000000000..cd5d1b2c1af4c
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/etc/analytics.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ reviews
+
+
+
+
+
+
+
+
+ rating_option_votes
+
+
+
+
+
diff --git a/app/code/Magento/ReviewAnalytics/etc/module.xml b/app/code/Magento/ReviewAnalytics/etc/module.xml
new file mode 100644
index 0000000000000..65df87bac4af1
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ReviewAnalytics/etc/reports.xml b/app/code/Magento/ReviewAnalytics/etc/reports.xml
new file mode 100644
index 0000000000000..8dd508983aced
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/etc/reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ReviewAnalytics/registration.php b/app/code/Magento/ReviewAnalytics/registration.php
new file mode 100644
index 0000000000000..6b795ca04c61b
--- /dev/null
+++ b/app/code/Magento/ReviewAnalytics/registration.php
@@ -0,0 +1,11 @@
+resultPageFactory->create(true);
$resultPage->addHandle('robots_index_index');
+ $resultPage->setHeader('Content-Type', 'text/plain');
return $resultPage;
}
}
diff --git a/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php
index 22a69cc13bd52..d3a7a97c7ea80 100644
--- a/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php
+++ b/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php
@@ -51,6 +51,9 @@ public function testExecute()
$resultPageMock->expects($this->once())
->method('addHandle')
->with('robots_index_index');
+ $resultPageMock->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Type', 'text/plain');
$this->resultPageFactory->expects($this->any())
->method('create')
diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/AbstractCreate.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/AbstractCreate.php
index 323c2df975a8a..b684fe8c62df2 100644
--- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/AbstractCreate.php
+++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/AbstractCreate.php
@@ -160,4 +160,22 @@ public function convertPrice($value, $format = true)
)
: $this->priceCurrency->convert($value, $this->getStore());
}
+
+ /**
+ * If item is quote or wishlist we need to get product from it.
+ *
+ * @param $item
+ *
+ * @return Product
+ */
+ public function getProduct($item)
+ {
+ if ($item instanceof Product) {
+ $product = $item;
+ } else {
+ $product = $item->getProduct();
+ }
+
+ return $product;
+ }
}
diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php
index c8afc6d8a292b..69f4d19e4dd63 100644
--- a/app/code/Magento/Sales/Model/AdminOrder/Create.php
+++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php
@@ -10,9 +10,12 @@
use Magento\Customer\Api\AddressMetadataInterface;
use Magento\Customer\Model\Metadata\Form as CustomerForm;
+use Magento\Framework\Api\ExtensibleDataObjectConverter;
use Magento\Framework\App\ObjectManager;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Item;
+use Magento\Sales\Api\Data\OrderAddressInterface;
+use Magento\Sales\Model\Order;
/**
* Order create model
@@ -235,6 +238,11 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\
*/
private $serializer;
+ /**
+ * @var ExtensibleDataObjectConverter
+ */
+ private $dataObjectConverter;
+
/**
* @param \Magento\Framework\ObjectManagerInterface $objectManager
* @param \Magento\Framework\Event\ManagerInterface $eventManager
@@ -265,6 +273,7 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\
* @param \Magento\Quote\Model\QuoteFactory $quoteFactory
* @param array $data
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
+ * @param ExtensibleDataObjectConverter|null $dataObjectConverter
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -296,7 +305,8 @@ public function __construct(
\Magento\Sales\Api\OrderManagementInterface $orderManagement,
\Magento\Quote\Model\QuoteFactory $quoteFactory,
array $data = [],
- \Magento\Framework\Serialize\Serializer\Json $serializer = null
+ \Magento\Framework\Serialize\Serializer\Json $serializer = null,
+ ExtensibleDataObjectConverter $dataObjectConverter = null
) {
$this->_objectManager = $objectManager;
$this->_eventManager = $eventManager;
@@ -328,6 +338,8 @@ public function __construct(
$this->serializer = $serializer ?: ObjectManager::getInstance()
->get(\Magento\Framework\Serialize\Serializer\Json::class);
parent::__construct($data);
+ $this->dataObjectConverter = $dataObjectConverter ?: ObjectManager::getInstance()
+ ->get(ExtensibleDataObjectConverter::class);
}
/**
@@ -514,9 +526,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order)
$shippingAddress = $order->getShippingAddress();
if ($shippingAddress) {
- $addressDiff = array_diff_assoc($shippingAddress->getData(), $order->getBillingAddress()->getData());
- unset($addressDiff['address_type'], $addressDiff['entity_id']);
- $shippingAddress->setSameAsBilling(empty($addressDiff));
+ $shippingAddress->setSameAsBilling($this->isAddressesAreEqual($order));
}
$this->_initBillingAddressFromOrder($order);
@@ -1914,6 +1924,7 @@ public function createOrder()
$oldOrder = $this->getSession()->getOrder();
$oldOrder->setRelationChildId($order->getId());
$oldOrder->setRelationChildRealId($order->getIncrementId());
+ $oldOrder->save();
$this->orderManagement->cancel($oldOrder->getEntityId());
$order->save();
}
@@ -2009,4 +2020,26 @@ protected function _getNewCustomerEmail()
return $email;
}
+
+ /**
+ * Checks id shipping and billing addresses are equal.
+ *
+ * @param Order $order
+ * @return bool
+ */
+ private function isAddressesAreEqual(Order $order)
+ {
+ $shippingAddress = $order->getShippingAddress();
+ $billingAddress = $order->getBillingAddress();
+ $shippingData = $this->dataObjectConverter->toFlatArray($shippingAddress, [], OrderAddressInterface::class);
+ $billingData = $this->dataObjectConverter->toFlatArray($billingAddress, [], OrderAddressInterface::class);
+ unset(
+ $shippingData['address_type'],
+ $shippingData['entity_id'],
+ $billingData['address_type'],
+ $billingData['entity_id']
+ );
+
+ return $shippingData == $billingData;
+ }
}
diff --git a/app/code/Magento/Sales/Model/Order/CustomerManagement.php b/app/code/Magento/Sales/Model/Order/CustomerManagement.php
index a84d90693e087..bf54e65d0ce10 100644
--- a/app/code/Magento/Sales/Model/Order/CustomerManagement.php
+++ b/app/code/Magento/Sales/Model/Order/CustomerManagement.php
@@ -3,8 +3,10 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Sales\Model\Order;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\AlreadyExistsException;
use Magento\Quote\Model\Quote\Address as QuoteAddress;
@@ -43,6 +45,11 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn
*/
protected $objectCopyService;
+ /**
+ * @var \Magento\Quote\Model\Quote\AddressFactory
+ */
+ private $quoteAddressFactory;
+
/**
* @param \Magento\Framework\DataObject\Copy $objectCopyService
* @param \Magento\Customer\Api\AccountManagementInterface $accountManagement
@@ -50,6 +57,7 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn
* @param \Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory
* @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionFactory
* @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
+ * @param \Magento\Quote\Model\Quote\AddressFactory|null $quoteAddressFactory
*/
public function __construct(
\Magento\Framework\DataObject\Copy $objectCopyService,
@@ -57,7 +65,8 @@ public function __construct(
\Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory,
\Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory,
\Magento\Customer\Api\Data\RegionInterfaceFactory $regionFactory,
- \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
+ \Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
+ \Magento\Quote\Model\Quote\AddressFactory $quoteAddressFactory = null
) {
$this->objectCopyService = $objectCopyService;
$this->accountManagement = $accountManagement;
@@ -65,6 +74,9 @@ public function __construct(
$this->customerFactory = $customerFactory;
$this->addressFactory = $addressFactory;
$this->regionFactory = $regionFactory;
+ $this->quoteAddressFactory = $quoteAddressFactory ?: ObjectManager::getInstance()->get(
+ \Magento\Quote\Model\Quote\AddressFactory::class
+ );
}
/**
@@ -84,6 +96,9 @@ public function create($orderId)
);
$addresses = $order->getAddresses();
foreach ($addresses as $address) {
+ if (!$this->isNeededToSaveAddress($address->getData('quote_address_id'))) {
+ continue;
+ }
$addressData = $this->objectCopyService->copyFieldsetToTarget(
'order_address',
'to_customer_address',
@@ -117,6 +132,26 @@ public function create($orderId)
$account = $this->accountManagement->createAccount($customer);
$order->setCustomerId($account->getId());
$this->orderRepository->save($order);
+
return $account;
}
+
+ /**
+ * Check if we need to save address in address book.
+ *
+ * @param int $quoteAddressId
+ *
+ * @return bool
+ */
+ private function isNeededToSaveAddress($quoteAddressId)
+ {
+ $saveInAddressBook = true;
+
+ $quoteAddress = $this->quoteAddressFactory->create()->load($quoteAddressId);
+ if ($quoteAddress && $quoteAddress->getId()) {
+ $saveInAddressBook = (int)$quoteAddress->getData('save_in_address_book');
+ }
+
+ return $saveInAddressBook;
+ }
}
diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php
index cb26fb611f219..64e20d5a69041 100644
--- a/app/code/Magento/Sales/Model/Order/Shipment.php
+++ b/app/code/Magento/Sales/Model/Order/Shipment.php
@@ -93,6 +93,11 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa
*/
protected $orderRepository;
+ /**
+ * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection|null
+ */
+ private $tracksCollection = null;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -331,16 +336,11 @@ public function addItem(\Magento\Sales\Model\Order\Shipment\Item $item)
*/
public function getTracksCollection()
{
- if (!$this->hasData(ShipmentInterface::TRACKS)) {
- $this->setTracks($this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()));
-
- if ($this->getId()) {
- foreach ($this->getTracks() as $track) {
- $track->setShipment($this);
- }
- }
+ if ($this->tracksCollection === null) {
+ $this->tracksCollection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId());
}
- return $this->getTracks();
+
+ return $this->tracksCollection;
}
/**
diff --git a/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrar.php b/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrar.php
index 9001267f6bc4a..69077749902b5 100644
--- a/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrar.php
+++ b/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrar.php
@@ -17,12 +17,19 @@ class OrderRegistrar implements \Magento\Sales\Model\Order\Shipment\OrderRegistr
*/
public function register(OrderInterface $order, ShipmentInterface $shipment)
{
- /** @var \Magento\Sales\Api\Data\ShipmentItemInterface|\Magento\Sales\Model\Order\Shipment\Item $item */
+ $totalQty = 0;
+ /** @var \Magento\Sales\Model\Order\Shipment\Item $item */
foreach ($shipment->getItems() as $item) {
if ($item->getQty() > 0) {
$item->register();
+
+ if (!$item->getOrderItem()->isDummy(true)) {
+ $totalQty += $item->getQty();
+ }
}
}
+ $shipment->setTotalQty($totalQty);
+
return $order;
}
}
diff --git a/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php
index 6d9bffdd00771..c0a3f84e8846d 100644
--- a/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php
+++ b/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php
@@ -121,7 +121,7 @@ private function getQuantitiesFromOrderItems(array $items)
{
$shipmentItems = [];
foreach ($items as $item) {
- if (!$item->getIsVirtual() && !$item->getParentItem()) {
+ if (!$item->getIsVirtual() && (!$item->getParentItem() || $item->isShipSeparately())) {
$shipmentItems[$item->getItemId()] = $item->getQtyOrdered();
}
}
diff --git a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php
index 0503343842c87..642f8647ef56b 100644
--- a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php
+++ b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php
@@ -96,15 +96,15 @@ protected function prepareItems(
\Magento\Sales\Model\Order $order,
array $items = []
) {
- $totalQty = 0;
+ $shipmentItems = [];
foreach ($order->getAllItems() as $orderItem) {
- if (!$this->canShipItem($orderItem, $items)) {
+ if ($this->validateItem($orderItem, $items) === false) {
continue;
}
/** @var \Magento\Sales\Model\Order\Shipment\Item $item */
$item = $this->converter->itemToShipmentItem($orderItem);
- if ($orderItem->getIsVirtual() || $orderItem->getParentItemId()) {
+ if ($orderItem->getIsVirtual() || ($orderItem->getParentItemId() && !$orderItem->isShipSeparately())) {
$item->isDeleted(true);
}
@@ -124,8 +124,7 @@ protected function prepareItems(
$qty = min($qty, $orderItem->getSimpleQtyToShip());
$item->setQty($this->castQty($orderItem, $qty));
- $shipment->addItem($item);
-
+ $shipmentItems[] = $item;
continue;
} else {
$qty = 1;
@@ -144,10 +143,65 @@ protected function prepareItems(
}
}
- $totalQty += $qty;
-
$item->setQty($this->castQty($orderItem, $qty));
- $shipment->addItem($item);
+ $shipmentItems[] = $item;
+ }
+ return $this->setItemsToShipment($shipment, $shipmentItems);
+ }
+
+ /**
+ * Validate order item before shipment
+ *
+ * @param Item $orderItem
+ * @param array $items
+ * @return bool
+ */
+ private function validateItem(\Magento\Sales\Model\Order\Item $orderItem, array $items)
+ {
+ if (!$this->canShipItem($orderItem, $items)) {
+ return false;
+ }
+
+ // Remove from shipment items without qty or with qty=0
+ if (!$orderItem->isDummy(true)
+ && (!isset($items[$orderItem->getId()]) || $items[$orderItem->getId()] <= 0)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set prepared items to shipment document
+ *
+ * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment
+ * @param array $shipmentItems
+ * @return \Magento\Sales\Api\Data\ShipmentInterface
+ */
+ private function setItemsToShipment(\Magento\Sales\Api\Data\ShipmentInterface $shipment, $shipmentItems)
+ {
+ $totalQty = 0;
+
+ /**
+ * Verify that composite products shipped separately has children, if not -> remove from collection
+ */
+ /** @var \Magento\Sales\Model\Order\Shipment\Item $shipmentItem */
+ foreach ($shipmentItems as $key => $shipmentItem) {
+ if ($shipmentItem->getOrderItem()->getHasChildren()
+ && $shipmentItem->getOrderItem()->isShipSeparately()
+ ) {
+ $containerId = $shipmentItem->getOrderItem()->getId();
+ $childItems = array_filter($shipmentItems, function ($item) use ($containerId) {
+ return $containerId == $item->getOrderItem()->getParentItemId();
+ });
+
+ if (count($childItems) <= 0) {
+ unset($shipmentItems[$key]);
+ continue;
+ }
+ }
+ $totalQty += $shipmentItem->getQty();
+ $shipment->addItem($shipmentItem);
}
return $shipment->setTotalQty($totalQty);
}
diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php
index 5851b2d936139..9c8671d02c578 100644
--- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php
+++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php
@@ -62,8 +62,8 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $object)
$this->shipmentItemResource->save($item);
}
}
- if (null !== $object->getTracks()) {
- foreach ($object->getTracks() as $track) {
+ if (null !== $object->getTracksCollection()) {
+ foreach ($object->getTracksCollection() as $track) {
$track->setParentId($object->getId());
$this->shipmentTrackResource->save($track);
}
diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedAtListProvider.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedAtListProvider.php
new file mode 100644
index 0000000000000..846fa46572fde
--- /dev/null
+++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedAtListProvider.php
@@ -0,0 +1,60 @@
+connection = $resourceConnection->getConnection('sales');
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getIds($mainTableName, $gridTableName)
+ {
+ $mainTableName = $this->resourceConnection->getTableName($mainTableName);
+ $gridTableName = $this->resourceConnection->getTableName($gridTableName);
+ $select = $this->connection->select()
+ ->from($mainTableName, [$mainTableName . '.entity_id'])
+ ->joinInner(
+ [$gridTableName => $gridTableName],
+ sprintf(
+ '%s.entity_id = %s.entity_id AND %s.updated_at > %s.updated_at',
+ $mainTableName,
+ $gridTableName,
+ $mainTableName,
+ $gridTableName
+ ),
+ []
+ );
+
+ return $this->connection->fetchAll($select, [], \Zend_Db::FETCH_COLUMN);
+ }
+}
diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php
index 59906c79215fa..42c6e9d650315 100644
--- a/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php
+++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php
@@ -38,10 +38,12 @@ public function __construct(
*/
public function getIds($mainTableName, $gridTableName)
{
+ $mainTableName = $this->resourceConnection->getTableName($mainTableName);
+ $gridTableName = $this->resourceConnection->getTableName($gridTableName);
$select = $this->getConnection()->select()
- ->from($this->getConnection()->getTableName($mainTableName), [$mainTableName . '.entity_id'])
+ ->from($mainTableName, [$mainTableName . '.entity_id'])
->joinLeft(
- [$gridTableName => $this->getConnection()->getTableName($gridTableName)],
+ [$gridTableName => $gridTableName],
sprintf(
'%s.%s = %s.%s',
$mainTableName,
diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php
index 2f08c26de9058..24f56c0dbd595 100644
--- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php
+++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php
@@ -195,7 +195,7 @@ public function refund(
*/
protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo)
{
- if ($creditmemo->getId()) {
+ if ($creditmemo->getId() && $creditmemo->getState() != \Magento\Sales\Model\Order\Creditmemo::STATE_OPEN) {
throw new \Magento\Framework\Exception\LocalizedException(
__('We cannot register an existing credit memo.')
);
diff --git a/app/code/Magento/Sales/Setup/UpgradeData.php b/app/code/Magento/Sales/Setup/UpgradeData.php
index 8b104b0b35590..1c36a9a538366 100644
--- a/app/code/Magento/Sales/Setup/UpgradeData.php
+++ b/app/code/Magento/Sales/Setup/UpgradeData.php
@@ -3,18 +3,25 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Sales\Setup;
use Magento\Eav\Model\Config;
+use Magento\Framework\App\State;
use Magento\Framework\DB\AggregatedFieldDataConverter;
use Magento\Framework\DB\DataConverter\SerializedToJson;
use Magento\Framework\DB\FieldToConvert;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\UpgradeDataInterface;
+use Magento\Quote\Model\QuoteFactory;
+use Magento\Sales\Model\OrderFactory;
+use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory;
/**
* Data upgrade script
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class UpgradeData implements UpgradeDataInterface
{
@@ -36,20 +43,50 @@ class UpgradeData implements UpgradeDataInterface
private $aggregatedFieldConverter;
/**
- * Constructor
- *
+ * @var AddressCollectionFactory
+ */
+ private $addressCollectionFactory;
+
+ /**
+ * @var OrderFactory
+ */
+ private $orderFactory;
+
+ /**
+ * @var QuoteFactory
+ */
+ private $quoteFactory;
+
+ /**
+ * @var State
+ */
+ private $state;
+
+ /**
* @param SalesSetupFactory $salesSetupFactory
* @param Config $eavConfig
* @param AggregatedFieldDataConverter $aggregatedFieldConverter
+ * @param AddressCollectionFactory $addressCollFactory
+ * @param OrderFactory $orderFactory
+ * @param QuoteFactory $quoteFactory
+ * @param State $state
*/
public function __construct(
SalesSetupFactory $salesSetupFactory,
Config $eavConfig,
- AggregatedFieldDataConverter $aggregatedFieldConverter
+ AggregatedFieldDataConverter $aggregatedFieldConverter,
+ AddressCollectionFactory $addressCollFactory,
+ OrderFactory $orderFactory,
+ QuoteFactory $quoteFactory,
+ State $state
) {
$this->salesSetupFactory = $salesSetupFactory;
$this->eavConfig = $eavConfig;
$this->aggregatedFieldConverter = $aggregatedFieldConverter;
+ $this->addressCollectionFactory = $addressCollFactory;
+ $this->orderFactory = $orderFactory;
+ $this->quoteFactory = $quoteFactory;
+ $this->state = $state;
}
/**
@@ -64,6 +101,13 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface
if (version_compare($context->getVersion(), '2.0.6', '<')) {
$this->convertSerializedDataToJson($context->getVersion(), $salesSetup);
}
+ if (version_compare($context->getVersion(), '2.0.8', '<')) {
+ $this->state->emulateAreaCode(
+ \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE,
+ [$this, 'fillQuoteAddressIdInSalesOrderAddress'],
+ [$setup]
+ );
+ }
$this->eavConfig->clear();
}
@@ -118,4 +162,34 @@ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSet
}
$this->aggregatedFieldConverter->convert($fieldsToUpdate, $salesSetup->getConnection());
}
+
+ /**
+ * Fill quote_address_id in table sales_order_address if it is empty.
+ */
+ public function fillQuoteAddressIdInSalesOrderAddress()
+ {
+ $addressCollection = $this->addressCollectionFactory->create();
+ /** @var \Magento\Sales\Model\Order\Address $orderAddress */
+ foreach ($addressCollection as $orderAddress) {
+ if (!$orderAddress->getData('quote_address_id')) {
+ $orderId = $orderAddress->getParentId();
+ $addressType = $orderAddress->getAddressType();
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $this->orderFactory->create()->load($orderId);
+ $quoteId = $order->getQuoteId();
+ $quote = $this->quoteFactory->create()->load($quoteId);
+
+ if ($addressType == \Magento\Sales\Model\Order\Address::TYPE_SHIPPING) {
+ $quoteAddressId = $quote->getShippingAddress()->getId();
+ $orderAddress->setData('quote_address_id', $quoteAddressId);
+ } elseif ($addressType == \Magento\Sales\Model\Order\Address::TYPE_BILLING) {
+ $quoteAddressId = $quote->getBillingAddress()->getId();
+ $orderAddress->setData('quote_address_id', $quoteAddressId);
+ }
+
+ $orderAddress->save();
+ }
+ }
+ }
}
diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/AbstractCreateTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/AbstractCreateTest.php
index 447fd7791ecbd..e010674ca354e 100644
--- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/AbstractCreateTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/AbstractCreateTest.php
@@ -3,8 +3,10 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Create;
+use Magento\Catalog\Model\Product;
use Magento\Catalog\Pricing\Price\FinalPrice;
class AbstractCreateTest extends \PHPUnit\Framework\TestCase
@@ -67,4 +69,34 @@ public function testGetItemPrice()
->willReturn($resultPrice);
$this->assertEquals($resultPrice, $this->model->getItemPrice($this->productMock));
}
+
+ /**
+ * @param $item
+ *
+ * @dataProvider getProductDataProvider
+ */
+ public function testGetProduct($item)
+ {
+ $product = $this->model->getProduct($item);
+
+ self::assertInstanceOf(Product::class, $product);
+ }
+
+ /**
+ * DataProvider for testGetProduct.
+ *
+ * @return array
+ */
+ public function getProductDataProvider()
+ {
+ $productMock = $this->createMock(Product::class);
+
+ $itemMock = $this->createMock(\Magento\Wishlist\Model\Item::class);
+ $itemMock->expects($this->once())->method('getProduct')->willReturn($productMock);
+
+ return [
+ [$productMock],
+ [$itemMock],
+ ];
+ }
}
diff --git a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php
index a265d39bafd93..b284a529d2a15 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php
@@ -8,8 +8,25 @@
namespace Magento\Sales\Test\Unit\Model\AdminOrder;
+use Magento\Backend\Model\Session\Quote as SessionQuote;
+use Magento\Customer\Api\Data\AttributeMetadataInterface;
+use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Customer\Api\Data\CustomerInterfaceFactory;
+use Magento\Customer\Api\Data\GroupInterface;
+use Magento\Customer\Api\GroupRepositoryInterface;
+use Magento\Customer\Model\Customer\Mapper;
+use Magento\Customer\Model\Metadata\Form;
+use Magento\Customer\Model\Metadata\FormFactory;
+use Magento\Framework\Api\DataObjectHelper;
+use Magento\Framework\App\RequestInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
+use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\Quote\Address;
+use Magento\Quote\Model\Quote\Item;
+use Magento\Quote\Model\Quote\Item\Updater;
+use Magento\Sales\Model\AdminOrder\Create;
use Magento\Sales\Model\AdminOrder\Product;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -19,161 +36,74 @@ class CreateTest extends \PHPUnit\Framework\TestCase
{
const CUSTOMER_ID = 1;
- /** @var \Magento\Sales\Model\AdminOrder\Create */
- protected $adminOrderCreate;
-
- /** @var \Magento\Backend\Model\Session\Quote|\PHPUnit_Framework_MockObject_MockObject */
- protected $sessionQuoteMock;
-
- /** @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject */
- protected $formFactoryMock;
-
- /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */
- protected $customerFactoryMock;
-
- /** @var \Magento\Quote\Model\Quote\Item\Updater|\PHPUnit_Framework_MockObject_MockObject */
- protected $itemUpdater;
-
- /** @var \Magento\Customer\Model\Customer\Mapper|\PHPUnit_Framework_MockObject_MockObject */
- protected $customerMapper;
-
/**
- * @var Product\Quote\Initializer|\PHPUnit_Framework_MockObject_MockObject
+ * @var Create
*/
- protected $quoteInitializerMock;
+ private $adminOrderCreate;
/**
- * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var SessionQuote|MockObject
*/
- protected $customerRepositoryMock;
+ private $sessionQuote;
/**
- * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var FormFactory|MockObject
*/
- protected $addressRepositoryMock;
+ private $formFactory;
/**
- * @var \Magento\Customer\Api\Data\AddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var CustomerInterfaceFactory|MockObject
*/
- protected $addressFactoryMock;
+ private $customerFactory;
/**
- * @var \Magento\Customer\Api\GroupRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var Updater|MockObject
*/
- protected $groupRepositoryMock;
+ private $itemUpdater;
/**
- * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var Mapper|MockObject
*/
- protected $scopeConfigMock;
+ private $customerMapper;
/**
- * @var \Magento\Sales\Model\AdminOrder\EmailSender|\PHPUnit_Framework_MockObject_MockObject
+ * @var GroupRepositoryInterface|MockObject
*/
- protected $emailSenderMock;
+ private $groupRepository;
/**
- * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var DataObjectHelper|MockObject
*/
- protected $accountManagementMock;
+ private $dataObjectHelper;
- /**
- * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject
- */
- protected $dataObjectHelper;
-
- /**
- * @var \PHPUnit_Framework_MockObject_MockObject
- */
- protected $objectFactory;
-
- /**
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
protected function setUp()
{
- $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class);
- $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class);
- $registryMock = $this->createMock(\Magento\Framework\Registry::class);
- $configMock = $this->createMock(\Magento\Sales\Model\Config::class);
- $this->sessionQuoteMock = $this->createMock(\Magento\Backend\Model\Session\Quote::class);
- $loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
- $copyMock = $this->createMock(\Magento\Framework\DataObject\Copy::class);
- $messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class);
- $this->formFactoryMock = $this->createPartialMock(\Magento\Customer\Model\Metadata\FormFactory::class, ['create']);
- $this->customerFactoryMock = $this->createPartialMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create']);
-
- $this->itemUpdater = $this->createMock(\Magento\Quote\Model\Quote\Item\Updater::class);
-
- $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class)
+ $this->sessionQuote = $this->createMock(SessionQuote::class);
+ $this->formFactory = $this->createPartialMock(FormFactory::class, ['create']);
+ $this->customerFactory = $this->createPartialMock(CustomerInterfaceFactory::class, ['create']);
+
+ $this->itemUpdater = $this->createMock(Updater::class);
+
+ $this->customerMapper = $this->getMockBuilder(Mapper::class)
+ ->setMethods(['toFlatArray'])
->disableOriginalConstructor()
- ->setMethods(['create'])
->getMock();
- $this->customerMapper = $this->getMockBuilder(
- \Magento\Customer\Model\Customer\Mapper::class
- )->setMethods(['toFlatArray'])->disableOriginalConstructor()->getMock();
-
- $this->quoteInitializerMock = $this->createMock(\Magento\Sales\Model\AdminOrder\Product\Quote\Initializer::class);
- $this->customerRepositoryMock = $this->getMockForAbstractClass(
- \Magento\Customer\Api\CustomerRepositoryInterface::class,
- [],
- '',
- false
- );
- $this->addressRepositoryMock = $this->getMockForAbstractClass(
- \Magento\Customer\Api\AddressRepositoryInterface::class,
- [],
- '',
- false
- );
- $this->addressFactoryMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterfaceFactory::class);
- $this->groupRepositoryMock = $this->getMockForAbstractClass(
- \Magento\Customer\Api\GroupRepositoryInterface::class,
- [],
- '',
- false
- );
- $this->scopeConfigMock = $this->getMockForAbstractClass(
- \Magento\Framework\App\Config\ScopeConfigInterface::class,
- [],
- '',
- false
- );
- $this->emailSenderMock = $this->createMock(\Magento\Sales\Model\AdminOrder\EmailSender::class);
- $this->accountManagementMock = $this->getMockForAbstractClass(
- \Magento\Customer\Api\AccountManagementInterface::class,
- [],
- '',
- false
- );
- $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class)
+ $this->groupRepository = $this->getMockForAbstractClass(GroupRepositoryInterface::class);
+ $this->dataObjectHelper = $this->getMockBuilder(DataObjectHelper::class)
->disableOriginalConstructor()
->getMock();
$objectManagerHelper = new ObjectManagerHelper($this);
$this->adminOrderCreate = $objectManagerHelper->getObject(
- \Magento\Sales\Model\AdminOrder\Create::class,
+ Create::class,
[
- 'objectManager' => $objectManagerMock,
- 'eventManager' => $eventManagerMock,
- 'coreRegistry' => $registryMock,
- 'salesConfig' => $configMock,
- 'quoteSession' => $this->sessionQuoteMock,
- 'logger' => $loggerMock,
- 'objectCopyService' => $copyMock,
- 'messageManager' => $messageManagerMock,
- 'quoteInitializer' => $this->quoteInitializerMock,
- 'customerRepository' => $this->customerRepositoryMock,
- 'addressRepository' => $this->addressRepositoryMock,
- 'addressFactory' => $this->addressFactoryMock,
- 'metadataFormFactory' => $this->formFactoryMock,
- 'customerFactory' => $this->customerFactoryMock,
- 'groupRepository' => $this->groupRepositoryMock,
+ 'quoteSession' => $this->sessionQuote,
+ 'metadataFormFactory' => $this->formFactory,
+ 'customerFactory' => $this->customerFactory,
+ 'groupRepository' => $this->groupRepository,
'quoteItemUpdater' => $this->itemUpdater,
'customerMapper' => $this->customerMapper,
- 'objectFactory' => $this->objectFactory,
- 'accountManagement' => $this->accountManagementMock,
'dataObjectHelper' => $this->dataObjectHelper,
]
);
@@ -188,64 +118,60 @@ public function testSetAccountData()
];
$attributeMocks = [];
- foreach ($attributes as $attribute) {
- $attributeMock = $this->createMock(\Magento\Customer\Api\Data\AttributeMetadataInterface::class);
+ foreach ($attributes as $value) {
+ $attribute = $this->createMock(AttributeMetadataInterface::class);
+ $attribute->method('getAttributeCode')
+ ->willReturn($value[0]);
- $attributeMock->expects($this->any())->method('getAttributeCode')->will($this->returnValue($attribute[0]));
-
- $attributeMocks[] = $attributeMock;
+ $attributeMocks[] = $attribute;
}
- $customerGroupMock = $this->getMockForAbstractClass(
- \Magento\Customer\Api\Data\GroupInterface::class,
- [],
- '',
- false,
- true,
- true,
- ['getTaxClassId']
- );
- $customerGroupMock->expects($this->once())->method('getTaxClassId')->will($this->returnValue($taxClassId));
- $customerFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class);
- $customerFormMock->expects($this->any())
- ->method('getAttributes')
- ->will($this->returnValue([$attributeMocks[1]]));
- $customerFormMock->expects($this->any())->method('extractData')->will($this->returnValue([]));
- $customerFormMock->expects($this->any())->method('restoreData')->will($this->returnValue(['group_id' => 1]));
-
- $customerFormMock->expects($this->any())
- ->method('prepareRequest')
- ->will($this->returnValue($this->createMock(\Magento\Framework\App\RequestInterface::class)));
-
- $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class);
- $this->customerMapper->expects($this->atLeastOnce())
+ $customerGroup = $this->getMockForAbstractClass(GroupInterface::class);
+ $customerGroup->method('getTaxClassId')
+ ->willReturn($taxClassId);
+ $customerForm = $this->createMock(Form::class);
+ $customerForm->method('getAttributes')
+ ->willReturn([$attributeMocks[1]]);
+ $customerForm
+ ->method('extractData')
+ ->willReturn([]);
+ $customerForm
+ ->method('restoreData')
+ ->willReturn(['group_id' => 1]);
+
+ $customerForm->method('prepareRequest')
+ ->willReturn($this->createMock(RequestInterface::class));
+
+ $customer = $this->createMock(CustomerInterface::class);
+ $this->customerMapper->expects(self::atLeastOnce())
->method('toFlatArray')
->willReturn(['group_id' => 1]);
- $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class);
- $quoteMock->expects($this->any())->method('getCustomer')->will($this->returnValue($customerMock));
- $quoteMock->expects($this->once())
- ->method('addData')
+ $quote = $this->createMock(Quote::class);
+ $quote->method('getCustomer')->willReturn($customer);
+ $quote->method('addData')
->with(
[
'customer_group_id' => $attributes[1][1],
'customer_tax_class_id' => $taxClassId
]
);
- $this->dataObjectHelper->expects($this->once())
- ->method('populateWithArray')
+ $this->dataObjectHelper->method('populateWithArray')
->with(
- $customerMock,
- ['group_id' => 1], \Magento\Customer\Api\Data\CustomerInterface::class
+ $customer,
+ ['group_id' => 1], CustomerInterface::class
);
- $this->formFactoryMock->expects($this->any())->method('create')->will($this->returnValue($customerFormMock));
- $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock));
- $this->customerFactoryMock->expects($this->any())->method('create')->will($this->returnValue($customerMock));
+ $this->formFactory->method('create')
+ ->willReturn($customerForm);
+ $this->sessionQuote
+ ->method('getQuote')
+ ->willReturn($quote);
+ $this->customerFactory->method('create')
+ ->willReturn($customer);
- $this->groupRepositoryMock->expects($this->once())
- ->method('getById')
- ->will($this->returnValue($customerGroupMock));
+ $this->groupRepository->method('getById')
+ ->willReturn($customerGroup);
$this->adminOrderCreate->setAccountData(['group_id' => 1]);
}
@@ -253,7 +179,7 @@ public function testSetAccountData()
public function testUpdateQuoteItemsNotArray()
{
$object = $this->adminOrderCreate->updateQuoteItems('string');
- $this->assertEquals($this->adminOrderCreate, $object);
+ self::assertEquals($this->adminOrderCreate, $object);
}
public function testUpdateQuoteItemsEmptyConfiguredOption()
@@ -266,22 +192,21 @@ public function testUpdateQuoteItemsEmptyConfiguredOption()
]
];
- $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class);
+ $item = $this->createMock(Item::class);
- $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class);
- $quoteMock->expects($this->once())
- ->method('getItemById')
- ->will($this->returnValue($itemMock));
+ $quote = $this->createMock(Quote::class);
+ $quote->method('getItemById')
+ ->willReturn($item);
- $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock));
- $this->itemUpdater->expects($this->once())
- ->method('update')
- ->with($this->equalTo($itemMock), $this->equalTo($items[1]))
- ->will($this->returnSelf());
+ $this->sessionQuote->method('getQuote')
+ ->willReturn($quote);
+ $this->itemUpdater->method('update')
+ ->with(self::equalTo($item), self::equalTo($items[1]))
+ ->willReturnSelf();
$this->adminOrderCreate->setRecollect(false);
$object = $this->adminOrderCreate->updateQuoteItems($items);
- $this->assertEquals($this->adminOrderCreate, $object);
+ self::assertEquals($this->adminOrderCreate, $object);
}
public function testUpdateQuoteItemsWithConfiguredOption()
@@ -295,43 +220,50 @@ public function testUpdateQuoteItemsWithConfiguredOption()
]
];
- $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class);
- $itemMock->expects($this->once())
- ->method('getQty')
- ->will($this->returnValue($qty));
+ $item = $this->createMock(Item::class);
+ $item->method('getQty')
+ ->willReturn($qty);
- $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class);
- $quoteMock->expects($this->once())
- ->method('updateItem')
- ->will($this->returnValue($itemMock));
+ $quote = $this->createMock(Quote::class);
+ $quote->method('updateItem')
+ ->willReturn($item);
- $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock));
+ $this->sessionQuote
+ ->method('getQuote')
+ ->willReturn($quote);
$expectedInfo = $items[1];
$expectedInfo['qty'] = $qty;
- $this->itemUpdater->expects($this->once())
- ->method('update')
- ->with($this->equalTo($itemMock), $this->equalTo($expectedInfo));
+ $this->itemUpdater->method('update')
+ ->with(self::equalTo($item), self::equalTo($expectedInfo));
$this->adminOrderCreate->setRecollect(false);
$object = $this->adminOrderCreate->updateQuoteItems($items);
- $this->assertEquals($this->adminOrderCreate, $object);
+ self::assertEquals($this->adminOrderCreate, $object);
}
public function testApplyCoupon()
{
- $couponCode = '';
- $quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getShippingAddress', 'setCouponCode']);
- $this->sessionQuoteMock->expects($this->once())->method('getQuote')->willReturn($quoteMock);
-
- $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['setCollectShippingRates', 'setFreeShipping']);
- $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($addressMock);
- $quoteMock->expects($this->once())->method('setCouponCode')->with($couponCode)->willReturnSelf();
-
- $addressMock->expects($this->once())->method('setCollectShippingRates')->with(true)->willReturnSelf();
- $addressMock->expects($this->once())->method('setFreeShipping')->with(0)->willReturnSelf();
+ $couponCode = '123';
+ $quote = $this->createPartialMock(Quote::class, ['getShippingAddress', 'setCouponCode']);
+ $this->sessionQuote->method('getQuote')
+ ->willReturn($quote);
+
+ $address = $this->createPartialMock(Address::class, ['setCollectShippingRates', 'setFreeShipping']);
+ $quote->method('getShippingAddress')
+ ->willReturn($address);
+ $quote->method('setCouponCode')
+ ->with($couponCode)
+ ->willReturnSelf();
+
+ $address->method('setCollectShippingRates')
+ ->with(true)
+ ->willReturnSelf();
+ $address->method('setFreeShipping')
+ ->with(0)
+ ->willReturnSelf();
$object = $this->adminOrderCreate->applyCoupon($couponCode);
- $this->assertEquals($this->adminOrderCreate, $object);
+ self::assertEquals($this->adminOrderCreate, $object);
}
}
diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php
index 8a2305543c490..2794860793ed6 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php
@@ -6,6 +6,7 @@
namespace Magento\Sales\Test\Unit\Model\Order;
use Magento\Quote\Model\Quote\Address;
+use Magento\Sales\Api\Data\OrderAddressInterface;
/**
* Class BuilderTest
@@ -49,6 +50,11 @@ class CustomerManagementTest extends \PHPUnit\Framework\TestCase
*/
protected $service;
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $quoteAddressFactory;
+
protected function setUp()
{
$this->objectCopyService = $this->createMock(\Magento\Framework\DataObject\Copy::class);
@@ -66,6 +72,7 @@ protected function setUp()
['create']
);
$this->orderRepository = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class);
+ $this->quoteAddressFactory = $this->createMock(\Magento\Quote\Model\Quote\AddressFactory::class);
$this->service = new \Magento\Sales\Model\Order\CustomerManagement(
$this->objectCopyService,
@@ -73,7 +80,8 @@ protected function setUp()
$this->customerFactory,
$this->addressFactory,
$this->regionFactory,
- $this->orderRepository
+ $this->orderRepository,
+ $this->quoteAddressFactory
);
}
@@ -94,12 +102,12 @@ public function testCreateCreatesCustomerBasedonGuestOrder()
$orderMock->expects($this->once())->method('getCustomerId')->will($this->returnValue(null));
$orderMock->expects($this->any())->method('getBillingAddress')->will($this->returnValue('billing_address'));
- $orderBillingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class);
+ $orderBillingAddress = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['getData']);
$orderBillingAddress->expects($this->once())
->method('getAddressType')
->willReturn(Address::ADDRESS_TYPE_BILLING);
- $orderShippingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class);
+ $orderShippingAddress = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['getData']);
$orderShippingAddress->expects($this->once())
->method('getAddressType')
->willReturn(Address::ADDRESS_TYPE_SHIPPING);
@@ -108,6 +116,17 @@ public function testCreateCreatesCustomerBasedonGuestOrder()
->method('getAddresses')
->will($this->returnValue([$orderBillingAddress, $orderShippingAddress]));
+ $billingQuoteAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
+ $billingQuoteAddress->expects($this->once())->method('load')->willReturn($billingQuoteAddress);
+ $billingQuoteAddress->expects($this->once())->method('getId')->willReturn(4);
+ $billingQuoteAddress->expects($this->once())->method('getData')->with('save_in_address_book')->willReturn(1);
+
+ $shippingQuoteAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
+ $shippingQuoteAddress->expects($this->once())->method('load')->willReturn($shippingQuoteAddress);
+ $shippingQuoteAddress->expects($this->once())->method('getId')->willReturn(5);
+ $shippingQuoteAddress->expects($this->once())->method('getData')->with('save_in_address_book')->willReturn(1);
+ $this->quoteAddressFactory->expects($this->exactly(2))->method('create')
+ ->willReturnOnConsecutiveCalls($billingQuoteAddress, $shippingQuoteAddress);
$this->orderRepository->expects($this->once())->method('get')->with(1)->will($this->returnValue($orderMock));
$this->objectCopyService->expects($this->any())->method('copyFieldsetToTarget')->will($this->returnValueMap(
[
@@ -142,4 +161,25 @@ public function testCreateCreatesCustomerBasedonGuestOrder()
$this->orderRepository->expects($this->once())->method('save')->with($orderMock);
$this->assertEquals($customerMock, $this->service->create(1));
}
+
+ /**
+ * Get mock for abstract class with methods.
+ *
+ * @param string $className
+ * @param array $methods
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createPartialMockForAbstractClass($className, $methods = [])
+ {
+ return $this->getMockForAbstractClass(
+ $className,
+ [],
+ '',
+ true,
+ true,
+ true,
+ $methods
+ );
+ }
}
diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php
index ecc37a2cd427d..9eb6be5f6d66e 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php
@@ -37,23 +37,20 @@ protected function setUp()
public function testRegister()
{
$item1 = $this->getShipmentItemMock();
- $item1->expects($this->once())
- ->method('getQty')
- ->willReturn(0);
- $item1->expects($this->never())
- ->method('register');
+ $item1->expects($this->once())->method('getQty')->willReturn(0);
+ $item1->expects($this->never())->method('register');
+ $item1->expects($this->never())->method('getOrderItem');
$item2 = $this->getShipmentItemMock();
- $item2->expects($this->once())
- ->method('getQty')
- ->willReturn(0.5);
- $item2->expects($this->once())
- ->method('register');
+ $item2->expects($this->atLeastOnce())->method('getQty')->willReturn(0.5);
+ $item2->expects($this->once())->method('register');
+
+ $orderItemMock = $this->createMock(\Magento\Sales\Model\Order\Item::class);
+ $orderItemMock->expects($this->once())->method('isDummy')->with(true)->willReturn(false);
+ $item2->expects($this->once())->method('getOrderItem')->willReturn($orderItemMock);
$items = [$item1, $item2];
- $this->shipmentMock->expects($this->once())
- ->method('getItems')
- ->willReturn($items);
+ $this->shipmentMock->expects($this->once())->method('getItems')->willReturn($items);
$this->assertEquals(
$this->orderMock,
$this->model->register($this->orderMock, $this->shipmentMock)
@@ -67,7 +64,7 @@ private function getShipmentItemMock()
{
return $this->getMockBuilder(\Magento\Sales\Api\Data\ShipmentItemInterface::class)
->disableOriginalConstructor()
- ->setMethods(['register'])
+ ->setMethods(['register', 'getOrderItem'])
->getMockForAbstractClass();
}
}
diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php
index 78e2483c6374e..e65b1b8330b93 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php
@@ -84,10 +84,18 @@ public function testCreate($tracks)
$orderItem->expects($this->any())->method('getParentItemId')->willReturn(false);
$orderItem->expects($this->any())->method('getIsVirtual')->willReturn(false);
- $shipmentItem = $this->createPartialMock(\Magento\Sales\Model\Order\Shipment\Item::class, ['setQty']);
+ $shipmentItem = $this->createPartialMock(
+ \Magento\Sales\Model\Order\Shipment\Item::class,
+ ['setQty', 'getOrderItem', 'getQty']
+ );
$shipmentItem->expects($this->once())
->method('setQty')
->with(5);
+ $shipmentItem->expects($this->once())
+ ->method('getQty')
+ ->willReturn(5);
+
+ $shipmentItem->expects($this->atLeastOnce())->method('getOrderItem')->willReturn($orderItem);
$order = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['getAllItems']);
$order->expects($this->any())
diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php
index 787e6f8e065d2..a7a615fb0f508 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php
@@ -86,7 +86,8 @@ protected function setUp()
'getId',
'getItems',
'getTracks',
- 'getComments'
+ 'getComments',
+ 'getTracksCollection',
]
)
->getMock();
@@ -123,7 +124,7 @@ public function testProcessRelations()
->method('getComments')
->willReturn([$this->commentMock]);
$this->shipmentMock->expects($this->exactly(2))
- ->method('getTracks')
+ ->method('getTracksCollection')
->willReturn([$this->trackMock]);
$this->itemMock->expects($this->once())
->method('setParentId')
diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Provider/NotSyncedDataProviderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Provider/NotSyncedDataProviderTest.php
index 8e248d239a501..5ed60f8de306f 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Provider/NotSyncedDataProviderTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Provider/NotSyncedDataProviderTest.php
@@ -5,15 +5,11 @@
*/
namespace Magento\Sales\Test\Unit\Model\ResourceModel\Provider;
-use Magento\Framework\ObjectManager\TMap;
use Magento\Framework\ObjectManager\TMapFactory;
use Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProvider;
use Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProviderInterface;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
-/**
- * Class NotSyncedDataProviderTest
- */
class NotSyncedDataProviderTest extends \PHPUnit\Framework\TestCase
{
public function testGetIdsEmpty()
@@ -23,30 +19,14 @@ public function testGetIdsEmpty()
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $tMap = $this->getMockBuilder(TMap::class)
- ->disableOriginalConstructor()
- ->getMock();
- $tMapFactory->expects(static::once())
- ->method('create')
- ->with(
- [
- 'array' => [],
- 'type' => NotSyncedDataProviderInterface::class
- ]
- )
- ->willReturn($tMap);
- $tMap->expects(static::once())
- ->method('getIterator')
- ->willReturn(new \ArrayIterator([]));
+ $tMapFactory->method('create')
+ ->willReturn([]);
- $provider = new NotSyncedDataProvider($tMapFactory, []);
- static::assertEquals([], $provider->getIds('main_table', 'grid_table'));
+ $provider = new NotSyncedDataProvider($tMapFactory);
+ self::assertEquals([], $provider->getIds('main_table', 'grid_table'));
}
- /**
- * @covers \Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProvider::getIds
- */
public function testGetIds()
{
/** @var TMapFactory|MockObject $tMapFactory */
@@ -54,46 +34,31 @@ public function testGetIds()
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $tMap = $this->getMockBuilder(TMap::class)
- ->disableOriginalConstructor()
- ->getMock();
$provider1 = $this->getMockBuilder(NotSyncedDataProviderInterface::class)
->getMockForAbstractClass();
- $provider1->expects(static::once())
- ->method('getIds')
+ $provider1->method('getIds')
->willReturn([1, 2]);
$provider2 = $this->getMockBuilder(NotSyncedDataProviderInterface::class)
->getMockForAbstractClass();
- $provider2->expects(static::once())
- ->method('getIds')
+ $provider2->method('getIds')
->willReturn([2, 3, 4]);
- $tMapFactory->expects(static::once())
- ->method('create')
- ->with(
+ $tMapFactory->method('create')
+ ->with(self::equalTo(
[
- 'array' => [
- 'provider1' => NotSyncedDataProviderInterface::class,
- 'provider2' => NotSyncedDataProviderInterface::class
- ],
+ 'array' => [$provider1, $provider2],
'type' => NotSyncedDataProviderInterface::class
]
- )
- ->willReturn($tMap);
- $tMap->expects(static::once())
- ->method('getIterator')
- ->willReturn(new \ArrayIterator([$provider1, $provider2]));
+ ))
+ ->willReturn([$provider1, $provider2]);
- $provider = new NotSyncedDataProvider(
- $tMapFactory,
- [
- 'provider1' => NotSyncedDataProviderInterface::class,
- 'provider2' => NotSyncedDataProviderInterface::class,
- ]
- );
+ $provider = new NotSyncedDataProvider($tMapFactory, [$provider1, $provider2]);
- static::assertEquals([1, 2, 3, 4], array_values($provider->getIds('main_table', 'grid_table')));
+ self::assertEquals(
+ [1, 2, 3, 4],
+ array_values($provider->getIds('main_table', 'grid_table'))
+ );
}
}
diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php
index 9ecab6cf9ab52..2e668f0b0d6f1 100644
--- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php
@@ -243,6 +243,78 @@ public function testRefund()
$this->assertSame($creditMemoMock, $this->creditmemoService->refund($creditMemoMock, true));
}
+ public function testRefundPendingCreditMemo()
+ {
+ $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class)
+ ->setMethods(['getId', 'getOrder', 'getState', 'getInvoice'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $creditMemoMock->expects($this->once())->method('getId')->willReturn(444);
+ $creditMemoMock->expects($this->once())->method('getState')
+ ->willReturn(\Magento\Sales\Model\Order\Creditmemo::STATE_OPEN);
+ $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock();
+
+ $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock);
+ $orderMock->expects($this->once())->method('getBaseTotalRefunded')->willReturn(0);
+ $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10);
+ $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10);
+
+ $this->priceCurrencyMock->expects($this->any())
+ ->method('round')
+ ->willReturnArgument(0);
+
+ // Set payment adapter dependency
+ $refundAdapterMock = $this->getMockBuilder(\Magento\Sales\Model\Order\RefundAdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper->setBackwardCompatibleProperty(
+ $this->creditmemoService,
+ 'refundAdapter',
+ $refundAdapterMock
+ );
+
+ // Set resource dependency
+ $resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper->setBackwardCompatibleProperty(
+ $this->creditmemoService,
+ 'resource',
+ $resourceMock
+ );
+
+ // Set order repository dependency
+ $orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper->setBackwardCompatibleProperty(
+ $this->creditmemoService,
+ 'orderRepository',
+ $orderRepositoryMock
+ );
+
+ $adapterMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $resourceMock->expects($this->once())->method('getConnection')->with('sales')->willReturn($adapterMock);
+ $adapterMock->expects($this->once())->method('beginTransaction');
+ $refundAdapterMock->expects($this->once())
+ ->method('refund')
+ ->with($creditMemoMock, $orderMock, false)
+ ->willReturn($orderMock);
+ $orderRepositoryMock->expects($this->once())
+ ->method('save')
+ ->with($orderMock);
+ $creditMemoMock->expects($this->once())
+ ->method('getInvoice')
+ ->willReturn(null);
+ $adapterMock->expects($this->once())->method('commit');
+ $this->creditmemoRepositoryMock->expects($this->once())
+ ->method('save');
+
+ $this->assertSame($creditMemoMock, $this->creditmemoService->refund($creditMemoMock, true));
+ }
+
/**
* @expectedExceptionMessage The most money available to refund is 1.
* @expectedException \Magento\Framework\Exception\LocalizedException
diff --git a/app/code/Magento/Sales/composer.json b/app/code/Magento/Sales/composer.json
index c4a99e5b45076..9761f82d1894a 100644
--- a/app/code/Magento/Sales/composer.json
+++ b/app/code/Magento/Sales/composer.json
@@ -32,7 +32,7 @@
"magento/module-sales-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "101.0.0",
+ "version": "101.0.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml
index 9de3f238d6a39..22ddc7b333574 100644
--- a/app/code/Magento/Sales/etc/di.xml
+++ b/app/code/Magento/Sales/etc/di.xml
@@ -470,6 +470,15 @@
CreditmemoRelationsComposite
+
+
+
+
+ - Magento\Sales\Model\ResourceModel\Provider\UpdatedIdListProvider
+ - Magento\Sales\Model\ResourceModel\Provider\UpdatedAtListProvider
+
+
+
sales_order
@@ -520,6 +529,7 @@
- sales_order_payment.method
- sales_order.total_refunded
+ Magento\Sales\Model\ResourceModel\Provider\NotSyncedOrderDataProvider
diff --git a/app/code/Magento/Sales/etc/fieldset.xml b/app/code/Magento/Sales/etc/fieldset.xml
index d680d7373bc35..0c8208b4167f5 100644
--- a/app/code/Magento/Sales/etc/fieldset.xml
+++ b/app/code/Magento/Sales/etc/fieldset.xml
@@ -279,7 +279,7 @@
-
+
diff --git a/app/code/Magento/Sales/etc/module.xml b/app/code/Magento/Sales/etc/module.xml
index 4c1a534faddf7..58c7a4f21202a 100644
--- a/app/code/Magento/Sales/etc/module.xml
+++ b/app/code/Magento/Sales/etc/module.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml
index 2ca2420934519..2dbf717f73439 100644
--- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml
+++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml
@@ -67,11 +67,7 @@
canDisplayPrice()): ?>
- getDataId() == 'cart'): ?>
- = /* @noEscape */ $block->getItemPrice($_item->getProduct()) ?>
-
- = /* @noEscape */ $block->getItemPrice($_item) ?>
-
+ = /* @noEscape */ $block->getItemPrice($block->getProduct($_item)) ?>
diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js
index 02529cee5c0ff..8990ea8d3f75b 100644
--- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js
+++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js
@@ -325,12 +325,12 @@ define([
data = this.serializeData(this.billingAddressContainer);
} else {
data = this.serializeData(this.shippingAddressContainer);
- areasToLoad.push('shipping_method');
}
+ areasToLoad.push('shipping_method');
data = data.toObject();
data['shipping_as_billing'] = flag ? 1 : 0;
data['reset_shipping'] = 1;
- this.loadArea( areasToLoad, true, data);
+ this.loadArea(areasToLoad, true, data);
},
resetShippingMethod : function(data){
diff --git a/app/code/Magento/SalesAnalytics/LICENSE.txt b/app/code/Magento/SalesAnalytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/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/SalesAnalytics/LICENSE_AFL.txt b/app/code/Magento/SalesAnalytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/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/SalesAnalytics/README.md b/app/code/Magento/SalesAnalytics/README.md
new file mode 100644
index 0000000000000..70f456c97d4b3
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/README.md
@@ -0,0 +1,3 @@
+# Magento_SalesAnalytics module
+
+The Magento_SalesAnalytics module configures data definitions for a data collection related to the Sales module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html).
diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json
new file mode 100644
index 0000000000000..344971d7056cf
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "magento/module-sales-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "magento/framework": "100.2.*",
+ "magento/module-sales": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\SalesAnalytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/SalesAnalytics/etc/analytics.xml b/app/code/Magento/SalesAnalytics/etc/analytics.xml
new file mode 100644
index 0000000000000..be6c4dfde9b19
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/etc/analytics.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ orders
+
+
+
+
+
+
+
+
+ order_items
+
+
+
+
+
+
+
+
+ order_addresses
+
+
+
+
+
diff --git a/app/code/Magento/SalesAnalytics/etc/module.xml b/app/code/Magento/SalesAnalytics/etc/module.xml
new file mode 100644
index 0000000000000..7a15075a4bc21
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/SalesAnalytics/etc/reports.xml b/app/code/Magento/SalesAnalytics/etc/reports.xml
new file mode 100644
index 0000000000000..bb6bdb800e9bf
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/etc/reports.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/SalesAnalytics/registration.php b/app/code/Magento/SalesAnalytics/registration.php
new file mode 100644
index 0000000000000..eff2c5b1a2c05
--- /dev/null
+++ b/app/code/Magento/SalesAnalytics/registration.php
@@ -0,0 +1,11 @@
+getChildren() as $child) {
- $ratio = $child->getBaseRowTotal() / $parentBaseRowTotal;
+ $ratio = $parentBaseRowTotal != 0 ? $child->getBaseRowTotal() / $parentBaseRowTotal : 0;
foreach ($keys as $key) {
if (!$item->hasData($key)) {
continue;
diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php
index 108cc341253ae..1e8fbf43ec3bc 100644
--- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php
+++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php
@@ -136,6 +136,7 @@ public function asHtml()
*
* @param \Magento\Framework\Model\AbstractModel $model
* @return bool
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function validate(\Magento\Framework\Model\AbstractModel $model)
{
@@ -145,8 +146,22 @@ public function validate(\Magento\Framework\Model\AbstractModel $model)
$attr = $this->getAttribute();
$total = 0;
foreach ($model->getQuote()->getAllVisibleItems() as $item) {
- if (parent::validate($item)) {
- $total += $item->getData($attr);
+ $hasValidChild = false;
+ $useChildrenTotal = ($item->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE);
+ $childrenAttrTotal = 0;
+ $children = $item->getChildren();
+ if (!empty($children)) {
+ foreach ($children as $child) {
+ if (parent::validate($child)) {
+ $hasValidChild = true;
+ if ($useChildrenTotal) {
+ $childrenAttrTotal += $child->getData($attr);
+ }
+ }
+ }
+ }
+ if ($hasValidChild || parent::validate($item)) {
+ $total += (($hasValidChild && $useChildrenTotal) ? $childrenAttrTotal : $item->getData($attr));
}
}
return $this->validateAttribute($total);
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php
index a1c325f39a947..671f20a27a460 100644
--- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php
@@ -229,7 +229,30 @@ public function collectItemHasChildrenDataProvider()
{
$data = [
// 3 items, each $100, testing that discount are distributed to item correctly
- 'three_items' => [
+ [
+ 'child_item_data' => [
+ 'item1' => [
+ 'base_row_total' => 0,
+ ]
+ ],
+ 'parent_item_data' => [
+ 'discount_amount' => 20,
+ 'base_discount_amount' => 10,
+ 'original_discount_amount' => 40,
+ 'base_original_discount_amount' => 20,
+ 'base_row_total' => 0,
+ ],
+ 'expected_child_item_data' => [
+ 'item1' => [
+ 'discount_amount' => 0,
+ 'base_discount_amount' => 0,
+ 'original_discount_amount' => 0,
+ 'base_original_discount_amount' => 0,
+ ]
+ ],
+ ],
+ [
+ // 3 items, each $100, testing that discount are distributed to item correctly
'child_item_data' => [
'item1' => [
'base_row_total' => 100,
diff --git a/app/code/Magento/Search/Helper/Data.php b/app/code/Magento/Search/Helper/Data.php
index f3ad8a39de00e..e813d7342761f 100644
--- a/app/code/Magento/Search/Helper/Data.php
+++ b/app/code/Magento/Search/Helper/Data.php
@@ -124,7 +124,7 @@ public function getSuggestUrl()
{
return $this->_getUrl(
'search/ajax/suggest',
- ['_secure' => $this->storeManager->getStore()->isCurrentlySecure()]
+ ['_secure' => $this->_getRequest()->isSecure()]
);
}
diff --git a/app/code/Magento/Search/Test/Unit/Helper/DataTest.php b/app/code/Magento/Search/Test/Unit/Helper/DataTest.php
index 5cfde7e8efda9..291362734feff 100644
--- a/app/code/Magento/Search/Test/Unit/Helper/DataTest.php
+++ b/app/code/Magento/Search/Test/Unit/Helper/DataTest.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Search\Test\Unit\Helper;
/**
@@ -43,6 +44,11 @@ class DataTest extends \PHPUnit\Framework\TestCase
*/
protected $storeManagerMock;
+ /**
+ * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $urlBuilderMock;
+
protected function setUp()
{
$this->stringMock = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class);
@@ -53,9 +59,14 @@ protected function setUp()
->disableOriginalConstructor()
->setMethods([])
->getMock();
+ $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class)
+ ->setMethods(['getUrl'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$this->contextMock = $this->createMock(\Magento\Framework\App\Helper\Context::class);
$this->contextMock->expects($this->any())->method('getScopeConfig')->willReturn($this->scopeConfigMock);
$this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock);
+ $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilderMock);
$this->model = new \Magento\Search\Helper\Data(
$this->contextMock,
@@ -126,4 +137,39 @@ public function queryTextDataProvider()
['testtest', 7, 'testtes'],
];
}
+
+ /**
+ * Test getSuggestUrl() take into consideration type of request(secure, non-secure).
+ *
+ * @dataProvider getSuggestUrlDataProvider
+ * @param bool $isSecure
+ * @return void
+ */
+ public function testGetSuggestUrl(bool $isSecure)
+ {
+ $this->requestMock->expects(self::once())
+ ->method('isSecure')
+ ->willReturn($isSecure);
+ $this->urlBuilderMock->expects(self::once())
+ ->method('getUrl')
+ ->with(self::identicalTo('search/ajax/suggest'), self::identicalTo(['_secure' => $isSecure]));
+ $this->model->getSuggestUrl();
+ }
+
+ /**
+ * Provide test data for testGetSuggestUrl() test.
+ *
+ * @return array
+ */
+ public function getSuggestUrlDataProvider()
+ {
+ return [
+ 'non-secure' => [
+ 'isSecure' => false,
+ ],
+ 'secure' => [
+ 'secure' => true,
+ ],
+ ];
+ }
}
diff --git a/app/code/Magento/Search/composer.json b/app/code/Magento/Search/composer.json
index 60ba6550cddf4..c6aecd101fc6f 100644
--- a/app/code/Magento/Search/composer.json
+++ b/app/code/Magento/Search/composer.json
@@ -11,7 +11,7 @@
"magento/module-ui": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml
index c4297b404717c..2ea87be13d5e3 100644
--- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml
+++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml
@@ -9,7 +9,7 @@
helper('Magento\Search\Helper\Data');
+$helper = $this->helper(\Magento\Search\Helper\Data::class);
?>
= /* @escapeNotVerified */ __('Search') ?>
@@ -23,7 +23,7 @@ $helper = $this->helper('Magento\Search\Helper\Data');
$block->getRequest()->isSecure()]) ?>",
+ "url":"= /* @escapeNotVerified */ $helper->getSuggestUrl()?>",
"destinationSelector":"#search_autocomplete"}
}'
type="text"
diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php
index 5520bae0e26a8..5eca94bce562b 100644
--- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php
+++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php
@@ -109,6 +109,8 @@ public function execute()
$isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label'];
+ $responseAjax = new \Magento\Framework\DataObject();
+
try {
$this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id'));
$this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id'));
@@ -143,7 +145,6 @@ public function execute()
$shipment->register();
$shipment->getOrder()->setCustomerNoteNotify(!empty($data['send_email']));
- $responseAjax = new \Magento\Framework\DataObject();
if ($isNeedCreateLabel) {
$this->labelGenerator->create($shipment, $this->_request);
diff --git a/app/code/Magento/Shipping/composer.json b/app/code/Magento/Shipping/composer.json
index 06fa1e5295c60..a7a2a3d48b06e 100644
--- a/app/code/Magento/Shipping/composer.json
+++ b/app/code/Magento/Shipping/composer.json
@@ -25,7 +25,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml
index f2de699fe1a2e..35e36aa5584c9 100644
--- a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml
+++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml
@@ -27,7 +27,7 @@
getShipment()->getAllItems() ?>
-
+ getOrderItem()->getParentItem()): continue; endif; $_i++ ?>
= $block->getItemHtml($_item) ?>
= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?>
diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml
index fc62e4d2fa6bb..8dddfaedda4e5 100644
--- a/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml
+++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml
@@ -16,7 +16,7 @@
getShipment()->getAllItems() ?>
-
+ getOrderItem()->getParentItem()): continue; endif; $_i++ ?>
= $block->getItemHtml($_item) ?>
= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?>
diff --git a/app/code/Magento/Signifyd/composer.json b/app/code/Magento/Signifyd/composer.json
index f11b23cc3b6c4..a162cef1614ca 100644
--- a/app/code/Magento/Signifyd/composer.json
+++ b/app/code/Magento/Signifyd/composer.json
@@ -3,21 +3,21 @@
"description": "Submitting Case Entry to Signifyd on Order Creation",
"require": {
"php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-config": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-sales": "100.2.*",
+ "magento/module-config": "101.0.*",
+ "magento/framework": "101.0.*",
+ "magento/module-sales": "101.0.*",
"magento/module-store": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-directory": "100.2.*",
"magento/module-checkout": "100.2.*",
"magento/module-backend": "100.2.*",
"magento/module-payment": "100.2.*"
},
"suggest": {
- "magento/module-config": "100.2.*"
+ "magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.1",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/Sitemap/Model/Observer.php b/app/code/Magento/Sitemap/Model/Observer.php
index 3ae3061310a0b..840a6a1858fae 100644
--- a/app/code/Magento/Sitemap/Model/Observer.php
+++ b/app/code/Magento/Sitemap/Model/Observer.php
@@ -113,7 +113,6 @@ public function scheduledGenerateSitemaps()
$sitemap->generateXml();
} catch (\Exception $e) {
$errors[] = $e->getMessage();
- throw $e;
}
}
@@ -122,8 +121,7 @@ public function scheduledGenerateSitemaps()
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
)
) {
- $translate = $this->_translateModel->getTranslateInline();
- $this->_translateModel->setTranslateInline(false);
+ $this->inlineTranslation->suspend();
$this->_transportBuilder->setTemplateIdentifier(
$this->_scopeConfig->getValue(
diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php
index 515caebe1ef27..f6a5f029eafca 100644
--- a/app/code/Magento/Sitemap/Model/Sitemap.php
+++ b/app/code/Magento/Sitemap/Model/Sitemap.php
@@ -43,6 +43,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento
const TYPE_URL = 'url';
+ /**
+ * Last mode date min value
+ */
+ const LAST_MOD_MIN_VAL = '0000-01-01 00:00:00';
+
/**
* Real file path
*
@@ -157,6 +162,13 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento
*/
protected $_cacheTag = true;
+ /**
+ * Last mode min timestamp value
+ *
+ * @var int
+ */
+ private $lastModMinTsVal;
+
/**
* Initialize dependencies.
*
@@ -661,7 +673,11 @@ protected function _getMediaUrl($url)
*/
protected function _getFormattedLastmodDate($date)
{
- return date('c', strtotime($date));
+ if ($this->lastModMinTsVal === null) {
+ $this->lastModMinTsVal = strtotime(self::LAST_MOD_MIN_VAL);
+ }
+ $timestamp = max(strtotime($date), $this->lastModMinTsVal);
+ return date('c', $timestamp);
}
/**
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php
index 92e6f4e2e2293..ac88f23ff9d69 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php
@@ -7,6 +7,10 @@
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+/**
+ * Class ObserverTest
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class ObserverTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -96,11 +100,11 @@ protected function setUp()
);
}
- /**
- * @expectedException \Exception
- */
- public function testScheduledGenerateSitemapsThrowsException()
+ public function testScheduledGenerateSitemapsSendsExceptionEmail()
{
+ $exception = 'Sitemap Exception';
+ $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class);
+
$this->scopeConfigMock->expects($this->once())->method('isSetFlag')->willReturn(true);
$this->collectionFactoryMock->expects($this->once())
@@ -111,7 +115,55 @@ public function testScheduledGenerateSitemapsThrowsException()
->method('getIterator')
->willReturn(new \ArrayIterator([$this->sitemapMock]));
- $this->sitemapMock->expects($this->once())->method('generateXml')->willThrowException(new \Exception());
+ $this->sitemapMock->expects($this->once())
+ ->method('generateXml')
+ ->willThrowException(new \Exception($exception));
+
+ $this->scopeConfigMock->expects($this->at(1))
+ ->method('getValue')
+ ->with(
+ \Magento\Sitemap\Model\Observer::XML_PATH_ERROR_RECIPIENT,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ )
+ ->willReturn('error-recipient@example.com');
+
+ $this->inlineTranslationMock->expects($this->once())
+ ->method('suspend');
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('setTemplateIdentifier')
+ ->will($this->returnSelf());
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('setTemplateOptions')
+ ->with([
+ 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE,
+ 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID,
+ ])
+ ->will($this->returnSelf());
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('setTemplateVars')
+ ->with(['warnings' => $exception])
+ ->will($this->returnSelf());
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->will($this->returnSelf());
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('addTo')
+ ->will($this->returnSelf());
+
+ $this->transportBuilderMock->expects($this->once())
+ ->method('getTransport')
+ ->willReturn($transport);
+
+ $transport->expects($this->once())
+ ->method('sendMessage');
+
+ $this->inlineTranslationMock->expects($this->once())
+ ->method('resume');
$this->observer->scheduledGenerateSitemaps();
}
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
index 40b72cbd53c00..83210c5789776 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
@@ -540,7 +540,7 @@ protected function _getModelMock($mockBeforeSave = false)
$this->returnValue(
[
new \Magento\Framework\DataObject(
- ['url' => 'product.html', 'updated_at' => '2012-12-21 00:00:00']
+ ['url' => 'product.html', 'updated_at' => '0000-00-00 00:00:00']
),
new \Magento\Framework\DataObject(
[
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml
index 3a0357cf30c51..519464cf76cf1 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml
@@ -10,7 +10,7 @@
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
http://store.com/product.html
- 2012-12-21T00:00:00-08:00
+ 0000-01-01T00:00:00-08:00
monthly
0.5
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
index e79e022c98995..cc2ff96dd28f2 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
@@ -22,7 +22,7 @@
http://store.com/product.html
- 2012-12-21T00:00:00-08:00
+ 0000-01-01T00:00:00-08:00
monthly
0.5
diff --git a/app/code/Magento/Sitemap/composer.json b/app/code/Magento/Sitemap/composer.json
index 71d9198a332a7..88be3616eebd0 100644
--- a/app/code/Magento/Sitemap/composer.json
+++ b/app/code/Magento/Sitemap/composer.json
@@ -18,7 +18,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Store/Block/Switcher.php b/app/code/Magento/Store/Block/Switcher.php
index 074dcb0262040..3d8d46983b5aa 100644
--- a/app/code/Magento/Store/Block/Switcher.php
+++ b/app/code/Magento/Store/Block/Switcher.php
@@ -223,9 +223,12 @@ public function getStoreName()
*/
public function getTargetStorePostData(\Magento\Store\Model\Store $store, $data = [])
{
- $data[\Magento\Store\Api\StoreResolverInterface::PARAM_NAME] = $store->getCode();
+ $data[\Magento\Store\Api\StoreResolverInterface::PARAM_NAME]
+ = $store->getCode();
+ //We need to fromStore as true because it will enable proper URL
+ //rewriting during store switching.
return $this->_postDataHelper->getPostData(
- $store->getCurrentUrl(false),
+ $store->getCurrentUrl(true),
$data
);
}
diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php
index aca8c4a0ba5ec..56751f2188411 100644
--- a/app/code/Magento/Store/Model/Store.php
+++ b/app/code/Magento/Store/Model/Store.php
@@ -1136,7 +1136,14 @@ public function isDefault()
public function getCurrentUrl($fromStore = true)
{
$sidQueryParam = $this->_sidResolver->getSessionIdQueryParam($this->_getSession());
- $requestString = $this->_url->escape(ltrim($this->_request->getRequestString(), '/'));
+ /** @var string $requestString Request path without query parameters */
+ $requestString = $this->_url->escape(
+ preg_replace(
+ '/\?.*?$/',
+ '',
+ ltrim($this->_request->getRequestString(), '/')
+ )
+ );
$storeUrl = $this->getUrl('', ['_secure' => $this->_storeManager->getStore()->isCurrentlySecure()]);
diff --git a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php
index 5f0ba6c0b42d3..8b4799d2b3437 100644
--- a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php
+++ b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php
@@ -53,7 +53,7 @@ public function testGetTargetStorePostData()
$storeSwitchUrl = 'http://domain.com/stores/store/switch';
$store->expects($this->atLeastOnce())
->method('getCurrentUrl')
- ->with(false)
+ ->with(true)
->willReturn($storeSwitchUrl);
$this->corePostDataHelper->expects($this->any())
->method('getPostData')
diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php
index aef54a47971ff..c05584c2d8bcb 100644
--- a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php
+++ b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php
@@ -370,10 +370,11 @@ public function testGetBaseUrlWrongType()
* @param boolean $secure
* @param string $url
* @param string $expected
+ * @param bool|string $fromStore
*/
- public function testGetCurrentUrl($secure, $url, $expected)
+ public function testGetCurrentUrl($secure, $url, $expected, $fromStore)
{
- $defaultStore = $this->createPartialMock(\Magento\Store\Model\Store::class, [
+ $defaultStore = $this->createPartialMock(Store::class, [
'getId',
'isCurrentlySecure',
'__wakeup'
@@ -386,15 +387,31 @@ public function testGetCurrentUrl($secure, $url, $expected)
$config = $this->getMockForAbstractClass(\Magento\Framework\App\Config\ReinitableConfigInterface::class);
- $this->requestMock->expects($this->atLeastOnce())->method('getRequestString')->will($this->returnValue(''));
+ $requestString = preg_replace(
+ '/http(s?)\:\/\/[a-z0-9\-]+\//i',
+ '',
+ $url
+ );
+ $this->requestMock
+ ->expects($this->atLeastOnce())
+ ->method('getRequestString')
+ ->willReturn($requestString);
$this->requestMock->expects($this->atLeastOnce())->method('getQueryValue')->will($this->returnValue([
'SID' => 'sid'
]));
$urlMock = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class);
- $urlMock->expects($this->atLeastOnce())->method('setScope')->will($this->returnSelf());
- $urlMock->expects($this->any())->method('getUrl')
- ->will($this->returnValue($url));
+ $urlMock
+ ->expects($this->atLeastOnce())
+ ->method('setScope')
+ ->will($this->returnSelf());
+ $urlMock->expects($this->any())
+ ->method('getUrl')
+ ->will($this->returnValue(str_replace($requestString, '', $url)));
+ $urlMock
+ ->expects($this->atLeastOnce())
+ ->method('escape')
+ ->willReturnArgument(0);
$storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class);
$storeManager->expects($this->any())
@@ -409,7 +426,7 @@ public function testGetCurrentUrl($secure, $url, $expected)
$model->setStoreId(2);
$model->setCode('scope_code');
- $this->assertEquals($expected, $model->getCurrentUrl(false));
+ $this->assertEquals($expected, $model->getCurrentUrl($fromStore));
}
/**
@@ -418,9 +435,31 @@ public function testGetCurrentUrl($secure, $url, $expected)
public function getCurrentUrlDataProvider()
{
return [
- [true, 'http://test/url', 'http://test/url?SID=sid&___store=scope_code'],
- [true, 'http://test/url?SID=sid1&___store=scope', 'http://test/url?SID=sid&___store=scope_code'],
- [false, 'https://test/url', 'https://test/url?SID=sid&___store=scope_code']
+ [
+ true,
+ 'http://test/url',
+ 'http://test/url?SID=sid&___store=scope_code',
+ false
+ ],
+ [
+ true,
+ 'http://test/url?SID=sid1&___store=scope',
+ 'http://test/url?SID=sid&___store=scope_code',
+ false
+ ],
+ [
+ false,
+ 'https://test/url',
+ 'https://test/url?SID=sid&___store=scope_code',
+ false
+ ],
+ [
+ true,
+ 'http://test/u/u.2?__store=scope_code',
+ 'http://test/u/u.2?'
+ . 'SID=sid&___store=scope_code&___from_store=old-store',
+ 'old-store'
+ ]
];
}
diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json
index 662414049c955..f9eb84e2f9fa1 100644
--- a/app/code/Magento/Store/composer.json
+++ b/app/code/Magento/Store/composer.json
@@ -14,7 +14,7 @@
"magento/module-deploy": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml
index 470337a97dcd9..bb5b23620df4b 100644
--- a/app/code/Magento/Store/etc/config.xml
+++ b/app/code/Magento/Store/etc/config.xml
@@ -115,6 +115,10 @@
php
+ php3
+ php4
+ php5
+ php7
htaccess
jsp
pl
diff --git a/app/code/Magento/Swagger/composer.json b/app/code/Magento/Swagger/composer.json
index 2429607dc8716..787d58891c9e0 100644
--- a/app/code/Magento/Swagger/composer.json
+++ b/app/code/Magento/Swagger/composer.json
@@ -6,7 +6,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
index aef8a25da2834..dfd3d6ce15f71 100644
--- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
+++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
@@ -119,6 +119,12 @@ public function __construct(
$configurableAttributeData,
$data
);
+
+ $this->addData(
+ [
+ 'cache_lifetime' => isset($data['cache_lifetime']) ? $data['cache_lifetime'] : 3600
+ ]
+ );
}
/**
diff --git a/app/code/Magento/Swatches/view/frontend/layout/checkout_cart_configure_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/checkout_cart_configure_type_configurable.xml
index 593ae32374417..62d6e57221f82 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/checkout_cart_configure_type_configurable.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/checkout_cart_configure_type_configurable.xml
@@ -6,7 +6,13 @@
*/
-->
-
-
-
+
+
+
+
+ false
+
+
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
index 28bf7baac0a36..9f47b4386c742 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
@@ -12,7 +12,11 @@
-
+
+
+ false
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
index 7ecd6558ef6ea..75a39a0e4e270 100644
--- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
+++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
@@ -4,7 +4,7 @@
* See COPYING.txt for license details.
*/
?>
-
+