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
@@ -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