workarea-save_for_later 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ decorate Admin::Dashboards::ReportsViewModel, with: :save_for_later do
3
+ def save_for_later_products
4
+ @save_for_later_products ||=
5
+ Admin::Reports::SaveForLaterProductsViewModel.wrap(
6
+ Workarea::Reports::SaveForLaterProducts.new(options),
7
+ options
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Workarea
2
+ module Admin
3
+ module Reports
4
+ class SaveForLaterProductsViewModel < ApplicationViewModel
5
+ def results
6
+ @results ||= model.results.map do |result|
7
+ product = products.detect { |p| p.id == result['_id'] }
8
+ OpenStruct.new({ product: product }.merge(result))
9
+ end
10
+ end
11
+
12
+ def products
13
+ @products ||= Catalog::Product.any_in(
14
+ id: model.results.map { |r| r['_id'] }
15
+ ).to_a
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -13,7 +13,6 @@ module Workarea
13
13
  def inventory
14
14
  @inventory ||= Inventory::Collection.new(model.items.map(&:sku))
15
15
  end
16
-
17
16
  end
18
17
  end
19
18
  end
@@ -0,0 +1,17 @@
1
+ .grid__cell
2
+ .card{ class: card_classes(:save_for_later_products_report, local_assigns[:active]) }
3
+ = link_to save_for_later_products_report_path, class: 'card__header' do
4
+ %span.card__header-text= t('workarea.admin.reports.save_for_later_products.title')
5
+ = inline_svg 'workarea/admin/icons/insights.svg', class: 'card__icon'
6
+
7
+ .card__body
8
+ .card__centered-content
9
+ %table
10
+ %tbody
11
+ - dashboard.save_for_later_products.results.take(4).each do |result|
12
+ %tr
13
+ %td= result.product.present? ? result.product.name : result._id
14
+ %td.align-right= number_to_currency(result.revenue)
15
+
16
+ = link_to save_for_later_products_report_path, class: 'card__button' do
17
+ %span.button.button--small= t('workarea.admin.dashboards.reports.view_full_report')
@@ -0,0 +1,22 @@
1
+ .insight
2
+ .insight__date
3
+ %span.insight__period= insight.reporting_on.strftime('%B %Y')
4
+ .insight__heading= t('workarea.admin.insights.most_saved_products.title')
5
+ .insight__body
6
+ %p.insight__note= t('workarea.admin.insights.most_saved_products.info')
7
+ .grid.grid--large.grid--center
8
+ - insight.results.each do |result|
9
+ .grid__cell.grid__cell--50.grid__cell--25-at-wide.align-center
10
+ .insight__product
11
+ - if result.product.blank?
12
+ = image_tag(product_image_url(Workarea::Catalog::ProductPlaceholderImage.cached, :small), alt: result.product_id, class: 'insight__product-image')
13
+ - else
14
+ = link_to catalog_product_path(result.product) do
15
+ = image_tag(product_image_url(result.product.primary_image, :small), alt: result.product.name, class: 'insight__product-image')
16
+ .insight__product-name
17
+ - if result.product.blank?
18
+ = result.product_id
19
+ - else
20
+ = link_to result.product.name, catalog_product_path(result.product)
21
+ .insight__product-info
22
+ %strong= t('workarea.admin.insights.most_saved_products.times', count: result.adds)
@@ -0,0 +1,38 @@
1
+ - @page_title = t('workarea.admin.reports.save_for_later_products.title')
2
+
3
+ .view
4
+ .view__header
5
+ .view__heading
6
+ = link_to "↑ #{t('workarea.admin.reports.all_reports')}", reports_dashboards_path
7
+ %h1.heading.heading--no-margin= t('workarea.admin.reports.save_for_later_products.title')
8
+ %p= t('workarea.admin.reports.reference_link_html', path: reference_report_path)
9
+
10
+ .view__container.view__container--narrow
11
+ .browsing-controls.browsing-controls--with-divider.browsing-controls--center.browsing-controls--filters-displayed
12
+ = form_tag save_for_later_products_report_path, method: 'get', class: 'browsing-controls__form' do
13
+ = render 'workarea/admin/shared/date_selector', starts_at: @report.starts_at, ends_at: @report.ends_at
14
+
15
+ .browsing-controls__count
16
+ = render_reports_results_message(@report)
17
+ = render 'workarea/admin/reports/export', report: @report
18
+
19
+ %table
20
+ %thead
21
+ %tr
22
+ %th= t('workarea.admin.fields.product')
23
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.adds'), report: @report, sort_by: 'adds'
24
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.deletes'), report: @report, sort_by: 'deletes'
25
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.units_sold'), report: @report, sort_by: 'units_sold'
26
+ %th.align-right= link_to_reports_sorting t('workarea.admin.fields.revenue'), report: @report, sort_by: 'revenue'
27
+ %tbody
28
+ - @report.results.each do |result|
29
+ %tr
30
+ %td
31
+ - if result.product.present?
32
+ = link_to result.product.name, catalog_product_path(result.product)
33
+ - else
34
+ = result._id
35
+ %td.align-center= number_with_delimiter result.adds
36
+ %td.align-center= number_with_delimiter result.deletes
37
+ %td.align-center= number_with_delimiter result.units_sold
38
+ %td.align-right= number_to_currency(result.revenue)
@@ -1,5 +1,5 @@
1
1
  .grid__cell
2
- = form_tag saved_list_items_path, method: 'post', class: 'action-group__item', data: { analytics: save_for_later_analytics_data(item).to_json } do
2
+ = form_tag saved_list_items_path, method: 'post', class: 'action-group__item', data: { analytics: add_to_saved_list_analytics_data(item.product).to_json } do
3
3
  = hidden_field_tag :product_id, item.product_id, id: nil
4
4
  = hidden_field_tag :sku, item.sku, id: nil
5
5
  = hidden_field_tag :quantity, item.quantity, id: nil
@@ -24,6 +24,8 @@
24
24
  = hidden_field_tag :sku, item.sku, id: nil
25
25
  = hidden_field_tag :quantity, item.quantity, id: nil
26
26
  = hidden_field_tag :saved_list_item_id, item.id, id: nil
27
+ = hidden_field_tag :via, current_saved_list.to_gid_param, id: nil
28
+
27
29
  - if item.customizations.present?
28
30
  - item.customizations.each do |key, value|
29
31
  = hidden_field_tag key, value
@@ -32,7 +34,8 @@
32
34
  - else
33
35
  %p.unavailable= t('workarea.storefront.products.unavailable')
34
36
  .grid__cell
35
- = link_to t('workarea.storefront.saved_lists.remove'), saved_list_item_path(item), data: { method: 'delete', confirm: t('workarea.storefront.saved_lists.confirm_remove') }
37
+ = form_tag saved_list_item_path(item), method: :delete, data: { deletion_form: { message: t('workarea.storefront.saved_lists.confirm_remove') }, analytics: remove_from_saved_list_analytics_data(item.product).to_json } do
38
+ = button_tag t('workarea.storefront.saved_lists.remove'), class: 'text-button'
36
39
 
37
40
  .product-list__item-cell
38
41
  %table.table
@@ -0,0 +1,21 @@
1
+ module Workarea
2
+ decorate SaveOrderMetrics, with: :save_for_later do
3
+ class_methods do
4
+ def save_catalog(metrics)
5
+ super
6
+
7
+ save_saved_lists(metrics)
8
+ end
9
+
10
+ def save_saved_lists(metrics)
11
+ metrics.products_from_saved_lists.each do |product_id, data|
12
+ Metrics::ProductByDay.inc(
13
+ key: { product_id: product_id },
14
+ at: metrics.placed_at,
15
+ **data
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -12,3 +12,13 @@ Workarea.append_stylesheets(
12
12
  'storefront.components',
13
13
  'workarea/storefront/save_for_later/components/product_prices'
14
14
  )
15
+
16
+ Workarea.append_javascripts(
17
+ 'storefront.modules',
18
+ 'workarea/storefront/save_for_later/modules/saved_list_analytics'
19
+ )
20
+
21
+ Workarea.append_partials(
22
+ 'admin.reports_dashboard',
23
+ 'workarea/admin/dashboards/save_for_later_products_card'
24
+ )
@@ -1,3 +1,7 @@
1
1
  Workarea.configure do |config|
2
- # Add custom configuration here
2
+ # How long to wait before clearing out old, anonymous saved list
3
+ # records. The default is 3 months, and this configures MongoDB's TTL
4
+ # for the collection. Indexes will need to be rebuilt if this option
5
+ # is changed.
6
+ config.saved_lists_expiration = 3.months
3
7
  end
@@ -1,5 +1,26 @@
1
1
  en:
2
2
  workarea:
3
+ admin:
4
+ fields:
5
+ adds: Adds
6
+ deletes: Deletes
7
+ insights:
8
+ most_saved_products:
9
+ title: Most Saved Products
10
+ info: Your products most frequently saved for later from the cart.
11
+ times:
12
+ one: '%{count} time'
13
+ other: '%{count} times'
14
+ reports:
15
+ reference:
16
+ adds:
17
+ name: Adds
18
+ description: The total number of items added to a list.
19
+ deletes:
20
+ name: Deletes
21
+ description: The total number of items deleted from a list
22
+ save_for_later_products:
23
+ title: Save For Later Products
3
24
  storefront:
4
25
  carts:
5
26
  save_for_later: Save for Later
@@ -1,5 +1,16 @@
1
1
  Workarea::Storefront::Engine.routes.draw do
2
2
  scope '(:locale)', constraints: Workarea::I18n.routes_constraint do
3
3
  resources :saved_list_items, only: [:create, :destroy]
4
+
5
+ post 'analytics/saved_list_add/:product_id', to: 'analytics#saved_list_add', as: :analytics_saved_list_add
6
+ post 'analytics/saved_list_remove/:product_id', to: 'analytics#saved_list_remove', as: :analytics_saved_list_remove
7
+ end
8
+ end
9
+
10
+ Workarea::Admin::Engine.routes.draw do
11
+ scope '(:locale)', constraints: Workarea::I18n.routes_constraint do
12
+ resource :report, only: [] do
13
+ get :save_for_later_products
14
+ end
4
15
  end
5
16
  end
@@ -1,5 +1,5 @@
1
1
  module Workarea
2
2
  module SaveForLater
3
- VERSION = '1.0.1'.freeze
3
+ VERSION = '1.2.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,9 @@
1
+ {
2
+ "devDependencies": {
3
+ "eslint": "~5.16.0",
4
+ "eslint-config-recommended": "~4.0.0",
5
+ "stylelint": "~10.1.0",
6
+ "stylelint-config-recommended-scss": "~3.3.0",
7
+ "stylelint-scss": "~3.9.2"
8
+ }
9
+ }
@@ -54,7 +54,7 @@ module Workarea
54
54
 
55
55
  assert_raises(Mongoid::Errors::DocumentNotFound) { list.reload }
56
56
 
57
- list = SavedList.find(user.id.to_s)
57
+ list = SavedList.find_by(user_id: user.id.to_s)
58
58
  assert_equal(1, list.items.count)
59
59
  end
60
60
  end
@@ -0,0 +1,63 @@
1
+ require 'test_helper'
2
+
3
+ module Workarea
4
+ module Insights
5
+ class MostSavedProductsTest < TestCase
6
+ setup :add_data, :time_travel
7
+
8
+ def add_data
9
+ Metrics::ProductByDay.inc(
10
+ key: { product_id: 'foo' },
11
+ at: Time.zone.local(2018, 10, 27),
12
+ saved_list_adds: 1
13
+ )
14
+
15
+ Metrics::ProductByDay.inc(
16
+ key: { product_id: 'foo' },
17
+ at: Time.zone.local(2018, 10, 28),
18
+ saved_list_adds: 2
19
+ )
20
+
21
+ Metrics::ProductByDay.inc(
22
+ key: { product_id: 'foo' },
23
+ at: Time.zone.local(2018, 10, 29),
24
+ saved_list_adds: 3
25
+ )
26
+
27
+ Metrics::ProductByDay.inc(
28
+ key: { product_id: 'bar' },
29
+ at: Time.zone.local(2018, 10, 27),
30
+ saved_list_adds: 3
31
+ )
32
+
33
+ Metrics::ProductByDay.inc(
34
+ key: { product_id: 'bar' },
35
+ at: Time.zone.local(2018, 10, 28),
36
+ saved_list_adds: 4
37
+ )
38
+
39
+ Metrics::ProductByDay.inc(
40
+ key: { product_id: 'bar' },
41
+ at: Time.zone.local(2018, 10, 29),
42
+ saved_list_adds: 5
43
+ )
44
+ end
45
+
46
+ def time_travel
47
+ travel_to Time.zone.local(2018, 11, 1)
48
+ end
49
+
50
+ def test_generate_monthly!
51
+ MostSavedProducts.generate_monthly!
52
+ assert_equal(1, MostSavedProducts.count)
53
+
54
+ saved_products = MostSavedProducts.first
55
+ assert_equal(2, saved_products.results.size)
56
+ assert_equal('bar', saved_products.results.first['product_id'])
57
+ assert_in_delta(12, saved_products.results.first['adds'])
58
+ assert_equal('foo', saved_products.results.second['product_id'])
59
+ assert_in_delta(6, saved_products.results.second['adds'])
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,122 @@
1
+ require 'test_helper'
2
+
3
+ module Workarea
4
+ module Reports
5
+ class SaveForLaterProductsTest < TestCase
6
+ setup :add_data, :time_travel
7
+
8
+ def add_data
9
+ Metrics::ProductByDay.inc(
10
+ key: { product_id: 'foo' },
11
+ at: Time.zone.local(2018, 10, 27),
12
+ saved_list_units_sold: 2,
13
+ saved_list_revenue: 10.to_m,
14
+ saved_list_adds: 2,
15
+ saved_list_deletes: 1
16
+ )
17
+
18
+ Metrics::ProductByDay.inc(
19
+ key: { product_id: 'foo' },
20
+ at: Time.zone.local(2018, 10, 28),
21
+ saved_list_units_sold: 4,
22
+ saved_list_revenue: 15.to_m,
23
+ saved_list_adds: 1,
24
+ saved_list_deletes: 0
25
+ )
26
+
27
+ Metrics::ProductByDay.inc(
28
+ key: { product_id: 'foo' },
29
+ at: Time.zone.local(2018, 10, 29),
30
+ saved_list_units_sold: 6,
31
+ saved_list_revenue: 27.to_m,
32
+ saved_list_adds: 1,
33
+ saved_list_deletes: 1
34
+ )
35
+
36
+ Metrics::ProductByDay.inc(
37
+ key: { product_id: 'bar' },
38
+ at: Time.zone.local(2018, 10, 27),
39
+ saved_list_units_sold: 3,
40
+ saved_list_revenue: 11.to_m,
41
+ saved_list_adds: 2,
42
+ saved_list_deletes: 2
43
+ )
44
+
45
+ Metrics::ProductByDay.inc(
46
+ key: { product_id: 'bar' },
47
+ at: Time.zone.local(2018, 10, 28),
48
+ saved_list_units_sold: 5,
49
+ saved_list_revenue: 15.to_m,
50
+ saved_list_adds: 1,
51
+ saved_list_deletes: 1
52
+ )
53
+
54
+ Metrics::ProductByDay.inc(
55
+ key: { product_id: 'bar' },
56
+ at: Time.zone.local(2018, 10, 29),
57
+ saved_list_units_sold: 7,
58
+ saved_list_revenue: 27.to_m,
59
+ saved_list_adds: 0,
60
+ saved_list_deletes: 1
61
+ )
62
+ end
63
+
64
+ def time_travel
65
+ travel_to Time.zone.local(2018, 10, 30)
66
+ end
67
+
68
+ def test_grouping_and_summing
69
+ report = SaveForLaterProducts.new
70
+ assert_equal(2, report.results.length)
71
+
72
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
73
+ assert_equal(12, foo['units_sold'])
74
+ assert_equal(52, foo['revenue'])
75
+ assert_equal(4, foo['adds'])
76
+ assert_equal(2, foo['deletes'])
77
+
78
+ bar = report.results.detect { |r| r['_id'] == 'bar' }
79
+ assert_equal(15, bar['units_sold'])
80
+ assert_equal(53, bar['revenue'])
81
+ assert_equal(3, bar['adds'])
82
+ assert_equal(4, bar['deletes'])
83
+ end
84
+
85
+ def test_date_ranges
86
+ report = SaveForLaterProducts.new
87
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
88
+ assert_equal(4, foo['adds'])
89
+
90
+ report = SaveForLaterProducts.new(starts_at: '2018-10-28', ends_at: '2018-10-28')
91
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
92
+ assert_equal(1, foo['adds'])
93
+
94
+ report = SaveForLaterProducts.new(starts_at: '2018-10-28', ends_at: '2018-10-29')
95
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
96
+ assert_equal(2, foo['adds'])
97
+
98
+ report = SaveForLaterProducts.new(starts_at: '2018-10-28')
99
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
100
+ assert_equal(2, foo['adds'])
101
+
102
+ report = SaveForLaterProducts.new(ends_at: '2018-10-28')
103
+ foo = report.results.detect { |r| r['_id'] == 'foo' }
104
+ assert_equal(3, foo['adds'])
105
+ end
106
+
107
+ def test_sorting
108
+ report = SaveForLaterProducts.new(sort_by: 'adds', sort_direction: 'asc')
109
+ assert_equal('bar', report.results.first['_id'])
110
+
111
+ report = SaveForLaterProducts.new(sort_by: 'adds', sort_direction: 'desc')
112
+ assert_equal('foo', report.results.first['_id'])
113
+
114
+ report = SaveForLaterProducts.new(sort_by: 'units_sold', sort_direction: 'asc')
115
+ assert_equal('foo', report.results.first['_id'])
116
+
117
+ report = SaveForLaterProducts.new(sort_by: 'units_sold', sort_direction: 'desc')
118
+ assert_equal('bar', report.results.first['_id'])
119
+ end
120
+ end
121
+ end
122
+ end