workarea-save_for_later 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.json +35 -0
  3. data/.github/workflows/ci.yml +60 -0
  4. data/.gitignore +1 -2
  5. data/.rubocop.yml +3 -0
  6. data/.stylelintrc.json +8 -0
  7. data/CHANGELOG.md +23 -4
  8. data/Gemfile +2 -9
  9. data/README.md +47 -1
  10. data/app/assets/javscripts/workarea/storefront/save_for_later/modules/saved_list_analytics.js +31 -0
  11. data/app/controllers/workarea/admin/reports_controller.decorator +10 -0
  12. data/app/controllers/workarea/storefront/analytics_controller.decorator +17 -0
  13. data/app/controllers/workarea/storefront/current_saved_list.rb +1 -1
  14. data/app/helpers/workarea/storefront/saved_lists_helper.rb +11 -3
  15. data/app/models/workarea/insights/most_saved_products.rb +32 -0
  16. data/app/models/workarea/metrics/product_by_day.decorator +10 -0
  17. data/app/models/workarea/saved_list.rb +12 -2
  18. data/app/queries/workarea/order_metrics.decorator +26 -0
  19. data/app/queries/workarea/reports/save_for_later_products.rb +51 -0
  20. data/app/view_models/workarea/admin/dashboards/reports_view_model.decorator +11 -0
  21. data/app/view_models/workarea/admin/reports/save_for_later_products_view_model.rb +20 -0
  22. data/app/view_models/workarea/storefront/saved_list_view_model.rb +0 -1
  23. data/app/views/workarea/admin/dashboards/_save_for_later_products_card.html.haml +17 -0
  24. data/app/views/workarea/admin/insights/_most_saved_products.html.haml +22 -0
  25. data/app/views/workarea/admin/reports/save_for_later_products.html.haml +38 -0
  26. data/app/views/workarea/storefront/carts/_save_for_later_button.html.haml +1 -1
  27. data/app/views/workarea/storefront/carts/_saved_for_later.html.haml +4 -1
  28. data/app/workers/workarea/save_order_metrics.decorator +21 -0
  29. data/config/initializers/appends.rb +10 -0
  30. data/config/initializers/config.rb +5 -1
  31. data/config/locales/en.yml +21 -0
  32. data/config/routes.rb +11 -0
  33. data/lib/workarea/save_for_later/version.rb +1 -1
  34. data/package.json +9 -0
  35. data/test/integration/workarea/storefront/current_saved_list_integration_test.rb +1 -1
  36. data/test/models/workarea/insights/most_saved_products_test.rb +63 -0
  37. data/test/queries/workarea/reports/save_for_later_products_test.rb +122 -0
  38. data/test/queries/workarea/save_for_later_order_metrics_test.rb +48 -0
  39. data/test/system/workarea/admin/save_for_later_products_report_system_test.rb +44 -0
  40. data/test/system/workarea/storefront/save_for_later_system_test.rb +1 -1
  41. data/test/system/workarea/storefront/saved_list_analytics_system_test.rb +76 -0
  42. data/workarea-save_for_later.gemspec +1 -3
  43. data/yarn.lock +3265 -0
  44. metadata +33 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bab92923c6b1840e8967b23fc09880497fd6e61a6c790672bd0b42e0cc89057
4
- data.tar.gz: f2cbe6190d8efafdabb2df3e48a4c4b87c77a95bf55a47a1496e94eceb2d2f42
3
+ metadata.gz: 3569051f8611040d8cc0eebad53a149062b5613136c7ccd35dec1d912e7fc055
4
+ data.tar.gz: 16cca8449ab063bb6b54c83f03d6da4d738768afaeaf6f0e0a35ff01838efaf2
5
5
  SHA512:
6
- metadata.gz: 79633fce99eaf90f567d5233cef2c1220098ebfa1d19a134b169b2b895d6e6258630ee9d6f2ffda34126d949f8f70ca227c0373917038302eec722efc8e0c565
7
- data.tar.gz: d688d251695fcf80c0912537b9e774f18fd767941505bb95f66be2898386dfec703ff15624dfa75cf329eb6fdbdd65f26df84e26f45211e4f3259e379678c171
6
+ metadata.gz: 419afeabf7b6908b740aa826349789aabe9f35ecdd53ac8f3f2242564b4799a21c396538818dfaf2d561e03930b39a887f0492bf18a429832276f7ef83b0c1cd
7
+ data.tar.gz: 9ed1b62784231a7983bd681bf4c08b44e3189830caf155921e10baf226de8ecdf130b6962e85ffb728a1f3cd5663da3f547d45b40f936a89915f41255aa3dca5
@@ -0,0 +1,35 @@
1
+ {
2
+ "extends": "eslint:recommended",
3
+ "rules": {
4
+ "semi": ["error", "always"],
5
+ "eqeqeq": ["error", "always"]
6
+ },
7
+ "globals": {
8
+ "window": true,
9
+ "document": true,
10
+ "WORKAREA": true,
11
+ "$": true,
12
+ "jQuery": true,
13
+ "_": true,
14
+ "feature": true,
15
+ "JST": true,
16
+ "Turbolinks": true,
17
+ "I18n": true,
18
+ "Chart": true,
19
+ "Dropzone": true,
20
+ "strftime": true,
21
+ "Waypoint": true,
22
+ "wysihtml": true,
23
+ "LocalTime": true,
24
+ "describe": true,
25
+ "after": true,
26
+ "afterEach": true,
27
+ "before": true,
28
+ "beforeEach": true,
29
+ "it": true,
30
+ "expect": true,
31
+ "sinon": true,
32
+ "fixture": true,
33
+ "chai": true
34
+ }
35
+ }
@@ -0,0 +1,60 @@
1
+ name: CI
2
+ on: [push]
3
+
4
+ jobs:
5
+ static_analysis:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - uses: actions/checkout@v1
9
+ - uses: workarea-commerce/ci/bundler-audit@v1
10
+ - uses: workarea-commerce/ci/rubocop@v1
11
+ - uses: workarea-commerce/ci/eslint@v1
12
+ with:
13
+ args: '**/*.js'
14
+ - uses: workarea-commerce/ci/stylelint@v1
15
+ with:
16
+ args: '**/*.scss'
17
+
18
+ admin_tests:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v1
22
+ - uses: actions/setup-ruby@v1
23
+ with:
24
+ ruby-version: 2.6.x
25
+ - uses: workarea-commerce/ci/test@v1
26
+ with:
27
+ command: bin/rails app:workarea:test:admin
28
+
29
+ core_tests:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v1
33
+ - uses: actions/setup-ruby@v1
34
+ with:
35
+ ruby-version: 2.6.x
36
+ - uses: workarea-commerce/ci/test@v1
37
+ with:
38
+ command: bin/rails app:workarea:test:core
39
+
40
+ storefront_tests:
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v1
44
+ - uses: actions/setup-ruby@v1
45
+ with:
46
+ ruby-version: 2.6.x
47
+ - uses: workarea-commerce/ci/test@v1
48
+ with:
49
+ command: bin/rails app:workarea:test:storefront
50
+
51
+ plugins_tests:
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v1
55
+ - uses: actions/setup-ruby@v1
56
+ with:
57
+ ruby-version: 2.6.x
58
+ - uses: workarea-commerce/ci/test@v1
59
+ with:
60
+ command: bin/rails app:workarea:test:plugins
data/.gitignore CHANGED
@@ -21,5 +21,4 @@ test/dummy/log/*.log
21
21
  test/dummy/db/*.sqlite3
22
22
  test/dummy/db/*.sqlite3-journal
23
23
  node_modules
24
- package.json
25
- yarn.lock
24
+ .rubocop-http*
@@ -0,0 +1,3 @@
1
+ inherit_from:
2
+ - https://raw.githubusercontent.com/workarea-commerce/workarea/master/.rubocop.yml
3
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "stylelint-config-recommended-scss",
3
+ "rules": {
4
+ "block-no-empty": null,
5
+ "no-descending-specificity": null,
6
+ "property-no-unknown": [true, { "ignoreProperties": ["mso-hide"] }]
7
+ }
8
+ }
@@ -1,9 +1,28 @@
1
- Workarea Save For Later 1.0.1 (2019-08-21)
1
+ Workarea Save For Later 1.1.0 (2019-03-13)
2
2
  --------------------------------------------------------------------------------
3
3
 
4
- * Open Source!
5
-
6
-
4
+ * Clean Saved Lists After Predetermined Time Range
5
+
6
+ Ensure that `Workarea::SavedList` records are removed after a
7
+ predetermined length of time (default: 3 months) by way of the [TTL
8
+ Indexes](https://docs.mongodb.com/manual/core/index-ttl/) feature of
9
+ MongoDB. Only Saved Lists that do not belong to a User will be cleaned
10
+ in this manner.
11
+
12
+ SAVE4LATER-4
13
+ Tom Scott
14
+
15
+ * Update for workarea v3.4 compatibility
16
+
17
+ SAVE4LATER-3
18
+ Matt Duffy
19
+
20
+ * Add details to README
21
+
22
+ SAVE4LATER-2
23
+ Matt Duffy
24
+
25
+
7
26
 
8
27
  Workarea Save For Later 1.0.0 (2018-08-08)
9
28
  --------------------------------------------------------------------------------
data/Gemfile CHANGED
@@ -1,17 +1,10 @@
1
1
  source 'https://rubygems.org'
2
- git_source(:github) { |repo| "git@github.com:#{repo}.git" }
2
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3
3
 
4
- # Declare your gem's dependencies in save_for_later.gemspec.
5
- # Bundler will treat runtime dependencies like base dependencies, and
6
- # development dependencies will be added by default to the :development group.
7
4
  gemspec
8
5
 
9
- # Declare any dependencies that are still in development here instead of in
10
- # your gemspec. These might include edge Rails or gems from your path or
11
- # Git. Remember to move these dependencies to your gemspec before releasing
12
- # your gem to rubygems.org.
13
-
14
6
  # To use a debugger
15
7
  # gem 'byebug', group: [:development, :test]
16
8
 
9
+ # gem 'workarea', '>= 3.4.0'
17
10
  gem 'workarea', github: 'workarea-commerce/workarea'
data/README.md CHANGED
@@ -1,7 +1,53 @@
1
1
  Workarea Save For Later
2
2
  ================================================================================
3
3
 
4
- Save For Later plugin for the Workarea platform.
4
+ Increase average order values and help retail customers save items for later without having to create an account.
5
+
6
+ This plugin allows a consumer with or without an account to move items from their cart to a "Saved For Later" list that displays on the cart page. For users without an account, this list is persisted through a cookie that will retain their list on their browser. For users with accounts, this list is tied to their account directly and will appear in any browser they log into allow quick access to items they may be considering for purchase.
7
+
8
+ Features
9
+ --------------------------------------------------------------------------------
10
+
11
+ * Each product in cart will display a “Save for later” link
12
+ * Moving a product from the regular cart to the saved cart will remove it from the cart
13
+ * Saved cart product list item will maintain SKU, quantity, and customizations
14
+ * Each product listed in the saved cart will display a “Move to cart” link
15
+ * Moving a product from the saved cart to the regular cart will remove it from the saved cart
16
+ * If a consumer has not saved any products to a saved cart the saved cart will not display
17
+ * Cookie-based list for guests, tied to account's for signed in users.
18
+ * Lists created while logged out will merge into a consumer's existing list if they log in after adding items to their "Saved for Later" list
19
+ * Utilizes existing cart HTML/CSS to add no additional components that need to be styled.
20
+ * Lists created by guest users are pruned from the database after a
21
+ predetermined length of time (**default:** 3 months)
22
+
23
+ Configuration
24
+ --------------------------------------------------------------------------------
25
+
26
+ `Workarea.config.saved_lists_expiration` configures the expiration time for `Workarea::SavedList` records. Using [MongoDB's TTL Indexes](https://docs.mongodb.com/manual/core/index-ttl/), we can leverage the database to prune these documents in a background thread when they are no longer relevant. This is used to define the `expireAfterSeconds` parameter on the TTL index, which means if this setting is changed, you'll have to rebuild indexes for the `SavedList` model like so:
27
+
28
+ ```ruby
29
+ Workarea::SavedList.remove_indexes
30
+ Workarea::SavedList.create_indexes
31
+ ```
32
+
33
+ To make sure your changes were taken up, view the specification for the TTL index like so:
34
+
35
+ ```ruby
36
+ Workarea::SavedList.index_specifications
37
+
38
+ => [#<Mongoid::Indexable::Specification:0x00007fd52a3b9078
39
+ @fields=[:user_id],
40
+ @key={:user_id=>1},
41
+ @klass=Workarea::SavedList,
42
+ @options={}>,
43
+ #<Mongoid::Indexable::Specification:0x00007fd52a3acc10
44
+ @fields=[:updated_at],
45
+ @key={:updated_at=>1},
46
+ @klass=Workarea::SavedList,
47
+ @options=
48
+ {:expire_after=>1 month, # <<< this is what should change
49
+ :partial_filter_expression=>{:user_id=>{"$eq"=>nil}}}>]
50
+ ```
5
51
 
6
52
  Getting Started
7
53
  --------------------------------------------------------------------------------
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @method
3
+ * @name registerAdapter
4
+ * @memberof WORKAREA.analytics
5
+ */
6
+ WORKAREA.analytics.registerAdapter('saved_lists', function () {
7
+ 'use strict';
8
+
9
+ return {
10
+ 'addToSavedList': function (payload) {
11
+ if (payload.id) {
12
+ $.ajax({
13
+ type: 'POST',
14
+ url: WORKAREA.routes.storefront.analyticsSavedListAddPath(
15
+ { product_id: payload.id }
16
+ ),
17
+ });
18
+ }
19
+ },
20
+ 'removeFromSavedList': function (payload) {
21
+ if (payload.id) {
22
+ $.ajax({
23
+ type: 'POST',
24
+ url: WORKAREA.routes.storefront.analyticsSavedListRemovePath(
25
+ { product_id: payload.id }
26
+ ),
27
+ });
28
+ }
29
+ }
30
+ };
31
+ });
@@ -0,0 +1,10 @@
1
+ module Workarea
2
+ decorate Admin::ReportsController, with: :save_for_later do
3
+ def save_for_later_products
4
+ @report = Admin::Reports::SaveForLaterProductsViewModel.wrap(
5
+ Workarea::Reports::SaveForLaterProducts.new(params),
6
+ view_model_options
7
+ )
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module Workarea
2
+ decorate Storefront::AnalyticsController, with: :save_for_later do
3
+ def saved_list_add
4
+ Metrics::ProductByDay.inc(
5
+ key: { product_id: params[:product_id] },
6
+ saved_list_adds: 1
7
+ )
8
+ end
9
+
10
+ def saved_list_remove
11
+ Metrics::ProductByDay.inc(
12
+ key: { product_id: params[:product_id] },
13
+ saved_list_deletes: 1
14
+ )
15
+ end
16
+ end
17
+ end
@@ -14,7 +14,7 @@ module Workarea
14
14
  def current_saved_list
15
15
  @current_saved_list ||= SavedListViewModel.wrap(
16
16
  if current_user.present?
17
- SavedList.find_or_create_by(id: current_user.id)
17
+ SavedList.find_or_create_by(user_id: current_user.id)
18
18
  elsif cookies[:saved_list_id].present?
19
19
  SavedList.find_or_create_by(id: cookies[:saved_list_id])
20
20
  else
@@ -1,11 +1,19 @@
1
1
  module Workarea
2
2
  module Storefront
3
3
  module SavedListsHelper
4
- def save_for_later_analytics_data(item)
4
+ def add_to_saved_list_analytics_data(product)
5
5
  {
6
- event: 'savedForLater',
6
+ event: 'addToSavedList',
7
7
  domEvent: 'submit',
8
- payload: order_item_analytics_data(item)
8
+ payload: product_analytics_data(product)
9
+ }
10
+ end
11
+
12
+ def remove_from_saved_list_analytics_data(product)
13
+ {
14
+ event: 'removeFromSavedList',
15
+ domEvent: 'submit',
16
+ payload: product_analytics_data(product)
9
17
  }
10
18
  end
11
19
  end
@@ -0,0 +1,32 @@
1
+ module Workarea
2
+ module Insights
3
+ class MostSavedProducts < Base
4
+ class << self
5
+ def dashboards
6
+ %w(catalog)
7
+ end
8
+
9
+ def generate_monthly!
10
+ results = generate_results
11
+ create!(results: results) if results.present?
12
+ end
13
+
14
+ def generate_results
15
+ report
16
+ .results
17
+ .take(Workarea.config.insights_products_list_max_results)
18
+ .map { |result| result.merge(product_id: result['_id']) }
19
+ end
20
+
21
+ def report
22
+ Reports::SaveForLaterProducts.new(
23
+ starts_at: beginning_of_last_month,
24
+ ends_at: end_of_last_month,
25
+ sort_by: 'adds',
26
+ sort_direction: 'desc'
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module Workarea
2
+ decorate Metrics::ProductByDay, with: :save_for_later do
3
+ decorated do
4
+ field :saved_list_adds, type: Integer, default: 0
5
+ field :saved_list_deletes, type: Integer, default: 0
6
+ field :saved_list_units_sold, type: Integer, default: 0
7
+ field :saved_list_revenue, type: Float, default: 0.0
8
+ end
9
+ end
10
+ end
@@ -2,11 +2,21 @@ module Workarea
2
2
  class SavedList
3
3
  include ApplicationDocument
4
4
 
5
- # Will be either a object id for guests, or a user's id
6
- field :_id, type: String, default: -> { BSON::ObjectId.new.to_s }
5
+ field :user_id, type: String
7
6
 
8
7
  embeds_many :items, class_name: 'Workarea::SavedList::Item'
9
8
 
9
+ index({ user_id: 1 })
10
+ index(
11
+ { updated_at: 1 },
12
+ {
13
+ expire_after_seconds: Workarea.config.saved_lists_expiration,
14
+ partial_filter_expression: {
15
+ user_id: { '$eq' => nil }
16
+ }
17
+ }
18
+ )
19
+
10
20
  def add_item(item_attributes = {})
11
21
  item_attributes = item_attributes.with_indifferent_access
12
22
  existing = items.where(item_attributes.slice(:sku, :customizations)).first
@@ -0,0 +1,26 @@
1
+ module Workarea
2
+ decorate OrderMetrics, with: :save_for_later do
3
+ def products_from_saved_lists
4
+ @products_from_saved_lists ||=
5
+ items_from_saved_list
6
+ .group_by(&:product_id)
7
+ .transform_values do |items|
8
+ values = calculate_based_on_items(items)
9
+
10
+ {
11
+ saved_list_units_sold: values[:units_sold],
12
+ saved_list_revenue: values[:revenue]
13
+ }
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def items_from_saved_list
20
+ @items_from_saved_list ||= items.select do |item|
21
+ via = GlobalID.parse(item.via)
22
+ via.present? && via.model_class.name == 'Workarea::SavedList'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module Workarea
2
+ module Reports
3
+ class SaveForLaterProducts
4
+ include Report
5
+
6
+ self.reporting_class = Metrics::ProductByDay
7
+ self.sort_fields = %w(revenue units_sold adds deletes)
8
+
9
+ def aggregation
10
+ [filter_results, project_used_fields, group_by_product]
11
+ end
12
+
13
+ def filter_results
14
+ {
15
+ '$match' => {
16
+ 'reporting_on' => { '$gte' => starts_at, '$lte' => ends_at },
17
+ '$or' => [
18
+ { 'saved_list_adds' => { '$gt' => 0 } },
19
+ { 'saved_list_deletes' => { '$gt' => 0 } },
20
+ { 'saved_list_units_sold' => { '$gt' => 0 } }
21
+ ]
22
+ }
23
+ }
24
+ end
25
+
26
+ def project_used_fields
27
+ {
28
+ '$project' => {
29
+ 'product_id' => 1,
30
+ 'saved_list_units_sold' => 1,
31
+ 'saved_list_adds' => 1,
32
+ 'saved_list_deletes' => 1,
33
+ 'saved_list_revenue' => 1
34
+ }
35
+ }
36
+ end
37
+
38
+ def group_by_product
39
+ {
40
+ '$group' => {
41
+ '_id' => '$product_id',
42
+ 'units_sold' => { '$sum' => '$saved_list_units_sold' },
43
+ 'adds' => { '$sum' => '$saved_list_adds' },
44
+ 'deletes' => { '$sum' => '$saved_list_deletes' },
45
+ 'revenue' => { '$sum' => '$saved_list_revenue' }
46
+ }
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end