workarea-api-storefront 4.4.7 → 4.5.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/workarea/api/storefront/analytics_controller.rb +31 -19
  3. data/app/controllers/workarea/api/storefront/application_controller.rb +15 -2
  4. data/app/controllers/workarea/api/storefront/authentication.rb +6 -9
  5. data/app/controllers/workarea/api/storefront/categories_controller.rb +3 -1
  6. data/app/controllers/workarea/api/storefront/checkouts_controller.rb +0 -1
  7. data/app/controllers/workarea/api/storefront/menus_controller.rb +3 -1
  8. data/app/controllers/workarea/api/storefront/pages_controller.rb +3 -1
  9. data/app/controllers/workarea/api/storefront/products_controller.rb +3 -1
  10. data/app/controllers/workarea/api/storefront/recent_views_controller.rb +13 -26
  11. data/app/controllers/workarea/api/storefront/recommendations_controller.rb +2 -8
  12. data/app/controllers/workarea/api/storefront/searches_controller.rb +0 -12
  13. data/app/views/workarea/api/storefront/categories/show.json.jbuilder +1 -1
  14. data/app/views/workarea/api/storefront/email_signups/show.json.jbuilder +1 -1
  15. data/app/views/workarea/api/storefront/menus/_menu.json.jbuilder +1 -1
  16. data/app/views/workarea/api/storefront/pages/show.json.jbuilder +1 -1
  17. data/app/views/workarea/api/storefront/recent_views/show.json.jbuilder +0 -1
  18. data/app/views/workarea/api/storefront/searches/show.json.jbuilder +1 -1
  19. data/app/views/workarea/api/storefront/system_content/show.json.jbuilder +1 -1
  20. data/config/initializers/config.rb +19 -0
  21. data/lib/workarea/api/storefront.rb +1 -0
  22. data/lib/workarea/api/storefront/visit.decorator +52 -0
  23. data/test/documentation/workarea/api/storefront/segmentation_documentation_test.rb +104 -0
  24. data/test/dummy/config/initializers/session_store.rb +3 -1
  25. data/test/integration/workarea/api/storefront/analytics_integration_test.rb +12 -7
  26. data/test/integration/workarea/api/storefront/recent_views_integration_test.rb +38 -33
  27. data/test/integration/workarea/api/storefront/recommendations_integration_test.rb +1 -1
  28. data/test/integration/workarea/api/storefront/searches_integration_test.rb +0 -36
  29. data/test/integration/workarea/api/storefront/segments_integration_test.rb +218 -0
  30. metadata +6 -5
  31. data/app/controllers/workarea/api/storefront/user_activity.rb +0 -36
  32. data/app/view_models/workarea/api/storefront/search_suggestion_view_model.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12dfdfb465b6dfb5eb2a2cd28dff9ee8cf6fc494c4e95db0413a389018623ca6
4
- data.tar.gz: 20e268c13c9fc5f3cfe2758ac3aa5120b2c2ca816e6cb43999f04bba113ec50a
3
+ metadata.gz: ae0d2a306dbdc516984394960bb8419f831836108a70e59d7df7c62653103770
4
+ data.tar.gz: 82ab83341a50980101a3e67bad447454c23825314bdb1c93ad0c525cbba8cabb
5
5
  SHA512:
6
- metadata.gz: f3a96efbc5c795bf35bc4d1d639a2f28e8131c6c862f724a380678d056c4ff6ede11ae75ddef23db777f1e62a0b28ff65e7f97ca2de4093f88e66eba8ac72e8a
7
- data.tar.gz: 924ffec40bca797c3950b59389084444fece3d626ae986a7098f3c382e43795e2da739e29bf6cd9656d98b53e071a4b65f8d60edb410619d90b507e6604eebfb
6
+ metadata.gz: f2f47996cf078f62e2ba219f6904f2e7d0d29311bfb84b0ec2f1b6be6a303ae2aaba86ea8c34abcb245eb056479b72e2cea252a945dcb7518579d9264a0bb9db
7
+ data.tar.gz: d93ffbe23d2e961d49ae0b8d3f4955c4a804a36a2b6780ce11e819756c4ecaabed70f243ef950c0297e173aa24e6fd80db4d29da49052eb030191f15c9af7674
@@ -1,38 +1,50 @@
1
1
  module Workarea
2
2
  module Api
3
3
  module Storefront
4
- class AnalyticsController < ActionController::Metal
5
- include ActionController::Head
6
- include ActionController::Instrumentation
7
-
4
+ class AnalyticsController < Api::Storefront::ApplicationController
8
5
  def category_view
9
6
  Metrics::CategoryByDay.inc(key: { category_id: params[:category_id] }, views: 1)
7
+
8
+ if current_metrics_id.present?
9
+ Metrics::User.save_affinity(
10
+ id: current_metrics_id,
11
+ action: 'viewed',
12
+ category_ids: params[:category_id]
13
+ )
14
+ end
15
+
10
16
  head :ok
11
17
  end
12
18
 
13
19
  def product_view
14
20
  Metrics::ProductByDay.inc(key: { product_id: params[:product_id] }, views: 1)
21
+
22
+ if current_metrics_id.present?
23
+ Metrics::User.save_affinity(
24
+ id: current_metrics_id,
25
+ action: 'viewed',
26
+ product_ids: params[:product_id]
27
+ )
28
+ end
29
+
15
30
  head :ok
16
31
  end
17
32
 
18
33
  def search
19
- Metrics::SearchByDay.save_search(params[:q], params[:total_results])
20
- head :ok
21
- end
34
+ query_string = QueryString.new(params[:q])
22
35
 
23
- def search_abandonment
24
- warn <<~eos
25
- DEPRECATION WARNING: Search abandonment tracking is deprecated and will be removed \
26
- in Workarea 3.5.
27
- eos
28
- head :ok
29
- end
36
+ if query_string.present? && !query_string.short?
37
+ Metrics::SearchByDay.save_search(params[:q], params[:total_results])
38
+
39
+ if current_metrics_id.present?
40
+ Metrics::User.save_affinity(
41
+ id: current_metrics_id,
42
+ action: 'viewed',
43
+ search_ids: query_string.id
44
+ )
45
+ end
46
+ end
30
47
 
31
- def filters
32
- warn <<~eos
33
- DEPRECATION WARNING: Filter analytics tracking is deprecated and will be removed \
34
- in Workarea 3.5.
35
- eos
36
48
  head :ok
37
49
  end
38
50
  end
@@ -2,15 +2,15 @@ module Workarea
2
2
  module Api
3
3
  module Storefront
4
4
  class ApplicationController < Workarea::ApplicationController
5
- include Workarea::Storefront::HttpCaching
5
+ include HttpCaching
6
6
  include Api::Storefront::Authentication
7
- include Api::Storefront::UserActivity
8
7
 
9
8
  respond_to :json
10
9
 
11
10
  before_action :set_json_format
12
11
  before_action :skip_session
13
12
  before_action { params.permit! }
13
+ around_action :apply_segments
14
14
  after_action :disable_cors_protection
15
15
 
16
16
  rescue_from Mongoid::Errors::DocumentNotFound, with: :handle_not_found
@@ -28,6 +28,19 @@ module Workarea
28
28
  request.session_options[:skip] = true
29
29
  end
30
30
 
31
+ def assert_current_metrics_id
32
+ if current_metrics_id.blank?
33
+ render(
34
+ json: {
35
+ problem: t('workarea.api.storefront.recent_views.missing_id')
36
+ },
37
+ status: :unprocessable_entity
38
+ )
39
+
40
+ return false
41
+ end
42
+ end
43
+
31
44
  private
32
45
 
33
46
  def set_json_format
@@ -10,19 +10,16 @@ module Workarea
10
10
  helper_method :current_user
11
11
  end
12
12
 
13
- def current_user
14
- return @current_user if defined?(@current_user)
15
-
16
- user = authenticate_with_http_token do |token, options|
17
- User::AuthenticationToken.authenticate(token, options).try(:user)
18
- end
13
+ def self.find_user(token, options)
14
+ User::AuthenticationToken.authenticate(token, options).try(:user)
15
+ end
19
16
 
20
- @current_user = user || raise(InvalidError)
17
+ def current_user
18
+ current_visit.current_user || raise(InvalidError)
21
19
  end
22
20
 
23
21
  def authentication?
24
- regex = ActionController::HttpAuthentication::Token::TOKEN_REGEX
25
- request.authorization.to_s[regex].present?
22
+ current_visit.logged_in?
26
23
  end
27
24
  end
28
25
  end
@@ -5,7 +5,7 @@ module Workarea
5
5
  before_action :cache_page
6
6
 
7
7
  def index
8
- models = Catalog::Category.active.page(params[:page])
8
+ models = Catalog::Category.page(params[:page]).select(&:active?)
9
9
  @categories = Workarea::Storefront::CategoryViewModel.wrap(
10
10
  models,
11
11
  view_model_options
@@ -14,6 +14,8 @@ module Workarea
14
14
 
15
15
  def show
16
16
  model = Catalog::Category.find_by(slug: params[:id])
17
+ raise InvalidDisplay unless model.active?
18
+
17
19
  @category = Workarea::Storefront::CategoryViewModel.wrap(
18
20
  model,
19
21
  view_model_options
@@ -96,7 +96,6 @@ module Workarea
96
96
  if authentication?
97
97
  current_order.touch_checkout!(
98
98
  ip_address: request.remote_ip,
99
- user_activity_id: current_user.id,
100
99
  checkout_by_id: current_user.id,
101
100
  source: 'storefront_api'
102
101
  )
@@ -10,7 +10,9 @@ module Workarea
10
10
  end
11
11
 
12
12
  def show
13
- model = Navigation::Menu.active.find(params[:id])
13
+ model = Navigation::Menu.find(params[:id])
14
+ raise InvalidDisplay unless model.active?
15
+
14
16
  @menu = Workarea::Storefront::MenuViewModel.wrap(model, params)
15
17
  end
16
18
  end
@@ -5,7 +5,9 @@ module Workarea
5
5
  before_action :cache_page
6
6
 
7
7
  def show
8
- model = Content::Page.active.find_by(slug: params[:id])
8
+ model = Content::Page.find_by(slug: params[:id])
9
+ raise InvalidDisplay unless model.active?
10
+
9
11
  @page = Workarea::Storefront::PageViewModel.new(model, view_model_options)
10
12
  end
11
13
  end
@@ -5,7 +5,9 @@ module Workarea
5
5
  before_action :cache_page
6
6
 
7
7
  def show
8
- model = Workarea::Catalog::Product.active.find_by(slug: params[:id])
8
+ model = Workarea::Catalog::Product.find_by(slug: params[:id])
9
+ raise InvalidDisplay unless model.active?
10
+
9
11
  @product = Workarea::Storefront::ProductViewModel.wrap(
10
12
  model,
11
13
  view_model_options
@@ -2,39 +2,26 @@ module Workarea
2
2
  module Api
3
3
  module Storefront
4
4
  class RecentViewsController < Api::Storefront::ApplicationController
5
- before_action :assert_current_user_activity_id
5
+ before_action :assert_current_metrics_id
6
6
 
7
7
  def show
8
- if stale?(
9
- etag: user_activity,
10
- last_modified: user_activity.updated_at,
11
- public: true
8
+ @recent_views = Workarea::Storefront::UserActivityViewModel.new(
9
+ current_metrics
12
10
  )
13
- @recent_views = Workarea::Storefront::UserActivityViewModel.new(
14
- user_activity
15
- )
16
- end
17
11
  end
18
12
 
19
13
  def update
20
- if params[:product_id].present?
21
- Recommendation::UserActivity.save_product(
22
- current_user_activity_id,
23
- params[:product_id]
24
- )
25
- end
26
-
27
- if params[:category_id].present?
28
- Recommendation::UserActivity.save_category(
29
- current_user_activity_id,
30
- params[:category_id]
31
- )
32
- end
14
+ product_ids = Array.wrap(params[:product_id])
15
+ category_ids = Array.wrap(params[:category_id])
16
+ search_ids = Array.wrap(params[:search])
33
17
 
34
- if params[:search].present?
35
- Recommendation::UserActivity.save_search(
36
- current_user_activity_id,
37
- params[:search]
18
+ if current_metrics_id.present?
19
+ Metrics::User.save_affinity(
20
+ id: current_metrics_id,
21
+ action: 'viewed',
22
+ product_ids: product_ids,
23
+ category_ids: category_ids,
24
+ search_ids: search_ids
38
25
  )
39
26
  end
40
27
 
@@ -2,17 +2,11 @@ module Workarea
2
2
  module Api
3
3
  module Storefront
4
4
  class RecommendationsController < Api::Storefront::ApplicationController
5
- before_action :assert_current_user_activity_id
5
+ before_action :assert_current_metrics_id
6
6
 
7
7
  def show
8
- fresh_when(
9
- etag: user_activity,
10
- last_modified: user_activity.updated_at,
11
- public: true
12
- )
13
-
14
8
  @recommendations = Workarea::Storefront::PersonalizedRecommendationsViewModel.new(
15
- user_activity,
9
+ current_metrics,
16
10
  view_model_options
17
11
  )
18
12
  end
@@ -4,18 +4,6 @@ module Workarea
4
4
  class SearchesController < Api::Storefront::ApplicationController
5
5
  before_action :cache_page
6
6
 
7
- def index
8
- search_query = QueryString.new(params[:q]).sanitized
9
-
10
- render(nothing: true) && (return) if search_query.blank?
11
- autocomplete_params = params.permit(:q)
12
- search = Search::SearchSuggestions.new(autocomplete_params)
13
-
14
- @results = search.results.map do |result|
15
- SearchSuggestionViewModel.new(result).to_h
16
- end
17
- end
18
-
19
7
  def show
20
8
  response = Search::StorefrontSearch.new(params.to_unsafe_h).response
21
9
  @search = Workarea::Storefront::SearchViewModel.new(
@@ -14,7 +14,7 @@ json.cache! @category.cache_key, expires_in: 1.hour do
14
14
  json.partial! 'workarea/api/storefront/taxons/taxon', taxon: taxon
15
15
  end
16
16
 
17
- json.content_blocks @category.content.blocks do |block|
17
+ json.content_blocks @category.content.blocks.select(&:active?) do |block|
18
18
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
19
19
  end
20
20
 
@@ -1,4 +1,4 @@
1
1
  json.title @email_signup.title
2
- json.content_blocks @email_signup.content.blocks do |block|
2
+ json.content_blocks @email_signup.content.blocks.select(&:active?) do |block|
3
3
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
4
4
  end
@@ -5,7 +5,7 @@ json.cache! menu.cache_key, expires_in: 1.hour do
5
5
  json.url menu_path(menu)
6
6
  json.taxon_url menu.taxon.url.presence || storefront_api_url_for(menu.taxon)
7
7
 
8
- json.content_blocks menu.content.blocks do |block|
8
+ json.content_blocks menu.content.blocks.select(&:active?) do |block|
9
9
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
10
10
  end
11
11
  end
@@ -10,7 +10,7 @@ json.cache! @page.cache_key, expires_in: 1.hour do
10
10
  json.partial! 'workarea/api/storefront/taxons/taxon', taxon: taxon
11
11
  end
12
12
 
13
- json.content_blocks @page.content.blocks do |block|
13
+ json.content_blocks @page.content.blocks.select(&:active?) do |block|
14
14
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
15
15
  end
16
16
  end
@@ -5,4 +5,3 @@ end
5
5
  json.products @recent_views.products do |product|
6
6
  json.partial! 'workarea/api/storefront/products/product', product: product
7
7
  end
8
- json.searches @recent_views.searches
@@ -4,7 +4,7 @@ json.redirect @search.redirect
4
4
  json.sorts @search.sorts
5
5
  json.sort @search.sort
6
6
 
7
- json.content_blocks @search.content.blocks do |block|
7
+ json.content_blocks @search.content.blocks.select(&:active?) do |block|
8
8
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
9
9
  end
10
10
 
@@ -6,7 +6,7 @@ json.cache! @content.cache_key, expires_in: 1.hour do
6
6
  json.browser_title @content.browser_title
7
7
  json.meta_description @content.meta_description
8
8
 
9
- json.content_blocks @content.content.blocks do |block|
9
+ json.content_blocks @content.content.blocks.select(&:active?) do |block|
10
10
  json.partial! 'workarea/api/storefront/content_blocks/block', block: block
11
11
  end
12
12
  end
@@ -1,2 +1,21 @@
1
1
  Workarea.config.api_product_image_jobs_blacklist ||= %i[convert encode rotate optim avatar thumb]
2
2
  Workarea.config.authentication_token_expiration ||= 1.week
3
+
4
+ # Ok, this one's a doozy.
5
+ #
6
+ # To deliver segmentation in the storefront API, we need a way to change a
7
+ # {Visit}'s definition of things like sessions, cookies, auth, etc.
8
+ #
9
+ # Since segments are determined first (before any other middleware), we need a
10
+ # way to know whether this is an API request to for logic in {Visit}.
11
+ #
12
+ # Since this is before any other middleware (including Rails' routing), we don't
13
+ # have a way to check the controller class or anything else application-related
14
+ # for whether it's an API request or an ordinary request.
15
+ #
16
+ # The best thing I could come up with is this regex. This lambda provides a hook
17
+ # for builds in case it doesn't work. They can provide their own logic here.
18
+ #
19
+ Workarea.config.is_api_visit = lambda do |request|
20
+ request.original_url =~ /:\/\/api\.|\/api\/./
21
+ end
@@ -13,3 +13,4 @@ end
13
13
 
14
14
  require 'workarea/api/version'
15
15
  require 'workarea/api/storefront/engine'
16
+ require 'workarea/api/storefront/visit.decorator'
@@ -0,0 +1,52 @@
1
+ module Workarea
2
+ decorate Visit, with: 'storefront_api' do
3
+ def api?
4
+ return @api if defined?(@api)
5
+ @api = Workarea.config.is_api_visit.call(request)
6
+ end
7
+
8
+ def cookies
9
+ api? ? {} : super
10
+ end
11
+
12
+ def session
13
+ api? ? {} : super
14
+ end
15
+
16
+ def logged_in?
17
+ return super unless api?
18
+
19
+ regex = ActionController::HttpAuthentication::Token::TOKEN_REGEX
20
+ request.authorization.to_s[regex].present?
21
+ end
22
+
23
+ def current_email
24
+ return super unless api?
25
+ return request.params['email'] unless logged_in?
26
+ return @current_email if defined? @current_email
27
+
28
+ @current_email = current_user&.email
29
+ end
30
+
31
+ def current_user
32
+ token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
33
+ @current_user ||= Api::Storefront::Authentication.find_user(token, options)
34
+ end
35
+
36
+ def sessions
37
+ api? ? request.params['sessions'].to_i : super
38
+ end
39
+
40
+ def current_metrics_id
41
+ return super unless api?
42
+
43
+ return @current_metrics_id if defined?(@current_metrics_id)
44
+ @current_metrics_id = current_email.presence || request.params['session_id']
45
+ end
46
+
47
+ def current_metrics_id=(value)
48
+ return super unless api?
49
+ # Unsupported in the API
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+ require 'workarea/api/documentation_test'
3
+
4
+ module Workarea
5
+ module Api
6
+ module Storefront
7
+ class SegmentationDocumentationTest < DocumentationTest
8
+ resource 'Segmentation'
9
+
10
+ def test_and_document_session_count
11
+ description 'Specifying session count for segmentation'
12
+ route storefront_api.system_content_path('home_page')
13
+ explanation <<~EOS
14
+ Workarea supports segmenting users by number of sessions, e.g.
15
+ first-time vs returning visitors. To support this in the storefront
16
+ API, the client will be responsible for managing this since the API
17
+ is stateless (doesn't have cookies/session).
18
+
19
+ To get session-based segments functioning, you need to pass a
20
+ `sessions` parameter in each request. Workarea will use that as
21
+ the number of sessions for determing the segments for the response.
22
+ This works for all requests across the storefront API.
23
+
24
+ This example shows getting a home page with segmented content based
25
+ on the number of sessions.
26
+ EOS
27
+
28
+ first_time = Segment::FirstTimeVisitor.instance
29
+ returning = Segment::ReturningVisitor.instance
30
+
31
+ content = Content.for('Home Page')
32
+ content.blocks.create!(
33
+ type: 'text',
34
+ data: { text: 'Hello visitor!' },
35
+ active_segment_ids: [first_time.id]
36
+ )
37
+ content.blocks.create!(
38
+ type: 'text',
39
+ data: { text: 'Welcome back!' },
40
+ active_segment_ids: [returning.id]
41
+ )
42
+
43
+ record_request do
44
+ get storefront_api.system_content_path('home_page', sessions: 1)
45
+ assert_match(/Hello visitor!/, response.body)
46
+ assert(response.ok?)
47
+ end
48
+ record_request do
49
+ get storefront_api.system_content_path('home_page', sessions: 2)
50
+ assert_match(/Welcome back!/, response.body)
51
+ assert(response.ok?)
52
+ end
53
+ end
54
+
55
+ def test_and_document_session_ids
56
+ description 'Using session IDs for segmenting non-authenticated users'
57
+ route storefront_api.system_content_path('home_page')
58
+ explanation <<~EOS
59
+ Workarea supports segmenting users by number of orders, revenue, etc.
60
+ For this functionality to work for users without accounts, you'll
61
+ need to maintain and pass a `session_id` consistently throughout calls
62
+ to the API.
63
+
64
+ An instance of `Metrics::User` will be found or created for that
65
+ `session_id`, and data about the user will be tracked there.
66
+
67
+ Assuming use of the `session_id` consistently through checkout, this
68
+ example shows getting home page content for two different anonymous
69
+ users and getting segmented content back.
70
+ EOS
71
+
72
+ first_time = Segment::FirstTimeCustomer.instance
73
+ returning = Segment::ReturningCustomer.instance
74
+
75
+ Metrics::User.create!(id: 'session_1', orders: 1)
76
+ Metrics::User.create!(id: 'session_2', orders: 2)
77
+
78
+ content = Content.for('Home Page')
79
+ content.blocks.create!(
80
+ type: 'text',
81
+ data: { text: 'Thanks for your order!' },
82
+ active_segment_ids: [first_time.id]
83
+ )
84
+ content.blocks.create!(
85
+ type: 'text',
86
+ data: { text: 'Welcome back repeat customer!' },
87
+ active_segment_ids: [returning.id]
88
+ )
89
+
90
+ record_request do
91
+ get storefront_api.system_content_path('home_page', session_id: 'session_1')
92
+ assert_match(/Thanks/, response.body)
93
+ assert(response.ok?)
94
+ end
95
+ record_request do
96
+ get storefront_api.system_content_path('home_page', session_id: 'session_2')
97
+ assert_match(/Welcome back/, response.body)
98
+ assert(response.ok?)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,3 +1,5 @@
1
1
  # Be sure to restart your server when you modify this file.
2
2
 
3
- Rails.application.config.session_store :cookie_store, key: '_dummy_session'
3
+ Rails.application.config.session_store :cookie_store,
4
+ key: '_dummy_session',
5
+ expire_after: 30.minutes
@@ -7,11 +7,19 @@ module Workarea
7
7
  def test_saving_category_view
8
8
  post storefront_api.analytics_category_view_path(category_id: 'foo')
9
9
  assert_equal(1, Metrics::CategoryByDay.first.views)
10
+
11
+ post storefront_api.analytics_category_view_path(category_id: 'foo', session_id: '1')
12
+ assert_equal(2, Metrics::CategoryByDay.first.views)
13
+ assert_equal(%w(foo), Metrics::User.find('1').viewed.category_ids)
10
14
  end
11
15
 
12
16
  def test_saving_product_view
13
17
  post storefront_api.analytics_product_view_path(product_id: 'foo')
14
18
  assert_equal(1, Metrics::ProductByDay.first.views)
19
+
20
+ post storefront_api.analytics_product_view_path(product_id: 'foo', session_id: '1')
21
+ assert_equal(2, Metrics::ProductByDay.first.views)
22
+ assert_equal(%w(foo), Metrics::User.find('1').viewed.product_ids)
15
23
  end
16
24
 
17
25
  def test_saving_search
@@ -21,14 +29,11 @@ module Workarea
21
29
  insights = Metrics::SearchByDay.first
22
30
  assert_equal(1, insights.searches)
23
31
  assert_equal(5, insights.total_results)
24
- end
25
32
 
26
- def test_saving_search_abandonment
27
- # Saving abandonment is deprecated, and this will be removed in v3.5
28
- end
29
-
30
- def test_saving_filters
31
- # Saving filters is deprecated, and this will be removed in v3.5
33
+ post storefront_api.analytics_search_path,
34
+ params: { q: 'foo', total_results: 5, session_id: '1' }
35
+ assert_equal(2, insights.reload.searches)
36
+ assert_equal(%w(foo), Metrics::User.find('1').viewed.search_ids)
32
37
  end
33
38
  end
34
39
  end
@@ -19,30 +19,38 @@ module Workarea
19
19
 
20
20
  def test_showing_recent_views_with_authentication
21
21
  user = create_user(first_name: 'Ben', last_name: 'Crouse')
22
- set_current_user(user)
22
+ post storefront_api.authentication_tokens_path,
23
+ params: { email: user.email, password: user.password }
24
+ token = JSON.parse(response.body)['token']
23
25
 
24
- activity = create_user_activity(
25
- id: user.id,
26
+ Metrics::User.save_affinity(
27
+ id: user.email,
28
+ action: 'viewed',
26
29
  product_ids: [@product.id],
27
30
  category_ids: [@category.id],
28
- searches: ['foo']
31
+ at: Time.current,
32
+ search_ids: %w(foo)
29
33
  )
30
34
 
31
- get storefront_api.recent_views_path
35
+ get storefront_api.recent_views_path,
36
+ headers: { 'HTTP_AUTHORIZATION' => encode_credentials(token) }
32
37
 
33
38
  assert(response.ok?)
34
39
  result = JSON.parse(response.body)
35
40
 
36
- assert_equal(activity.id, result['id'])
41
+ assert_equal(user.email, result['id'])
37
42
  assert_equal(@product.id, result['products'].first['id'])
38
43
  assert_equal(@category.id.to_s, result['categories'].first['id'])
39
- assert_includes(result['searches'], 'foo')
40
44
  end
41
45
 
42
46
  def test_adding_recent_views_for_authentication
43
47
  user = create_user(first_name: 'Ben', last_name: 'Crouse')
44
- set_current_user(user)
48
+ post storefront_api.authentication_tokens_path,
49
+ params: { email: user.email, password: user.password }
50
+ token = JSON.parse(response.body)['token']
51
+
45
52
  patch storefront_api.recent_views_path,
53
+ headers: { 'HTTP_AUTHORIZATION' => encode_credentials(token) },
46
54
  params: {
47
55
  product_id: @product.id,
48
56
  category_id: @category.id,
@@ -51,54 +59,51 @@ module Workarea
51
59
 
52
60
  assert(response.ok?)
53
61
 
54
- user_activity = Recommendation::UserActivity.first
55
- assert_equal(user.id.to_s, user_activity.id.to_s)
56
- assert_includes(user_activity.product_ids, @product.id)
57
- assert_includes(user_activity.category_ids, @category.id.to_s)
58
- assert_includes(user_activity.searches, 'foo')
62
+ user_activity = Workarea::Storefront::UserActivityViewModel.wrap(
63
+ Metrics::User.first
64
+ )
65
+ assert_equal(user.email, user_activity.id)
66
+ assert_includes(user_activity.products, @product)
67
+ assert_includes(user_activity.categories, @category)
59
68
  end
60
69
 
61
70
  def test_showing_recent_views_with_session_id
62
- activity = create_user_activity(
71
+ Metrics::User.save_affinity(
72
+ id: BSON::ObjectId.new.to_s,
73
+ action: 'viewed',
63
74
  product_ids: [@product.id],
64
75
  category_ids: [@category.id],
65
- searches: ['bar']
76
+ search_ids: %w(foo)
66
77
  )
78
+ activity = Metrics::User.last
67
79
 
68
80
  get storefront_api.recent_views_path,
69
81
  params: { session_id: activity.id }
70
82
 
71
- assert(response.ok?)
83
+ assert_response(:success)
72
84
  result = JSON.parse(response.body)
73
85
 
74
- assert_equal(activity.id, result['id'])
75
- assert_equal(@product.id, result['products'].first['id'])
86
+ assert_equal(activity.id.to_s, result['id'])
87
+ assert_equal(@product.id.to_s, result['products'].first['id'])
76
88
  assert_equal(@category.id.to_s, result['categories'].first['id'])
77
- assert_includes(result['searches'], 'bar')
78
89
  end
79
90
 
80
91
  def test_adding_recent_views_for_session_id
81
- activity = create_user_activity(
82
- product_ids: [@product.id],
83
- category_ids: [@category.id],
84
- searches: ['bar']
85
- )
86
-
87
92
  patch storefront_api.recent_views_path,
88
93
  params: {
89
- session_id: activity.id,
94
+ session_id: BSON::ObjectId.new.to_s,
90
95
  product_id: @product.id,
91
96
  category_id: @category.id,
92
97
  search: 'bar'
93
98
  }
94
99
 
95
- assert(response.ok?)
100
+ assert_response(:success)
96
101
 
97
- user_activity = Recommendation::UserActivity.first
98
- assert_equal(activity.id, user_activity.id)
99
- assert_includes(user_activity.product_ids, @product.id)
100
- assert_includes(user_activity.category_ids, @category.id.to_s)
101
- assert_includes(user_activity.searches, 'bar')
102
+ user_activity = Workarea::Storefront::UserActivityViewModel.wrap(
103
+ Metrics::User.first
104
+ )
105
+ assert_includes(user_activity.products, @product)
106
+ assert_includes(user_activity.categories, @category)
102
107
  end
103
108
 
104
109
  def test_adding_without_id
@@ -110,7 +115,7 @@ module Workarea
110
115
  }
111
116
 
112
117
  refute(response.ok?)
113
- assert_equal(0, Recommendation::UserActivity.count)
118
+ assert_equal(0, Metrics::User.count)
114
119
  end
115
120
  end
116
121
  end
@@ -23,7 +23,7 @@ module Workarea
23
23
  end
24
24
 
25
25
  def set_user_activity
26
- @activity = create_user_activity
26
+ @activity = Metrics::User.create!
27
27
  end
28
28
 
29
29
  def test_showing_recommendations_with_authentication
@@ -4,42 +4,6 @@ module Workarea
4
4
  module Api
5
5
  module Storefront
6
6
  class SearchesIntegrationTest < IntegrationTest
7
- def test_showing_search_autocomplete
8
- create_product(name: 'Foo')
9
- create_category(name: 'Foo Category')
10
- create_page(name: 'Foo Page')
11
-
12
- Metrics::SearchByDay.save_search('foo', 3)
13
- travel_to 1.weeks.from_now
14
- GenerateInsights.generate_all!
15
- BulkIndexSearches.perform
16
-
17
- get storefront_api.searches_path(q: 'foo')
18
- results = JSON.parse(response.body)['results']
19
- assert_equal(4, results.length)
20
-
21
- search = results.detect { |r| r['type'] == 'Searches' }
22
- assert(search.present?)
23
- assert_equal('foo', search['value'])
24
- assert_equal(storefront_api.search_path(q: 'foo'), search['url'])
25
-
26
- product = results.detect { |r| r['type'] == 'Products' }
27
- assert(product.present?)
28
- assert_equal('Foo', product['value'])
29
- assert_match(/product_images/, product['image'])
30
- assert_equal(storefront_api.product_path('foo'), product['url'])
31
-
32
- category = results.detect { |r| r['type'] == 'Categories' }
33
- assert(category.present?)
34
- assert_equal('Foo Category', category['value'])
35
- assert_equal(storefront_api.category_path('foo-category'), category['url'])
36
-
37
- page = results.detect { |r| r['type'] == 'Pages' }
38
- assert(page.present?)
39
- assert_equal('Foo Page', page['value'])
40
- assert_equal(storefront_api.page_path('foo-page'), page['url'])
41
- end
42
-
43
7
  def test_shows_search_results
44
8
  Search::Settings.current.update_attributes!(terms_facets: %w(Color Size))
45
9
  create_product(
@@ -0,0 +1,218 @@
1
+ require 'test_helper'
2
+
3
+ module Workarea
4
+ module Api
5
+ module Storefront
6
+ class SegmentsIntegrationTest < IntegrationTest
7
+ include AuthenticationTest
8
+
9
+ def test_sessions_functionality
10
+ first_time = Segment::FirstTimeVisitor.instance
11
+ returning = Segment::ReturningVisitor.instance
12
+
13
+ get storefront_api.system_content_path('home_page')
14
+ assert_equal(first_time.id.to_s, response.headers['X-Workarea-Segments'])
15
+
16
+ get storefront_api.system_content_path('home_page'), params: { sessions: 1 }
17
+ assert_equal(first_time.id.to_s, response.headers['X-Workarea-Segments'])
18
+
19
+ get storefront_api.system_content_path('home_page'), params: { sessions: 2 }
20
+ assert_equal(returning.id.to_s, response.headers['X-Workarea-Segments'])
21
+ end
22
+
23
+ def test_current_email_functionality
24
+ first_time = Segment::FirstTimeCustomer.instance
25
+ returning = Segment::ReturningCustomer.instance
26
+
27
+ Metrics::User.save_order(email: 'first-time@workarea.com', revenue: 1.to_m)
28
+ Metrics::User.save_order(email: 'returning@workarea.com', revenue: 1.to_m)
29
+ Metrics::User.save_order(email: 'returning@workarea.com', revenue: 1.to_m)
30
+
31
+ get storefront_api.system_content_path('home_page')
32
+ assert(response.headers['X-Workarea-Segments'].blank?)
33
+
34
+ get storefront_api.system_content_path('home_page'), params: { email: 'first-time@workarea.com' }
35
+ assert_equal(first_time.id.to_s, response.headers['X-Workarea-Segments'])
36
+
37
+ get storefront_api.system_content_path('home_page'), params: { email: 'returning@workarea.com' }
38
+ assert_equal(returning.id.to_s, response.headers['X-Workarea-Segments'])
39
+ end
40
+
41
+ def test_session_ids_for_metrics
42
+ first_time = Segment::FirstTimeCustomer.instance
43
+ returning = Segment::ReturningCustomer.instance
44
+
45
+ Metrics::User.create!(id: '1', orders: 1)
46
+ Metrics::User.create!(id: '2', orders: 2)
47
+
48
+ get storefront_api.system_content_path('home_page')
49
+ assert(response.headers['X-Workarea-Segments'].blank?)
50
+
51
+ get storefront_api.system_content_path('home_page'), params: { session_id: '1' }
52
+ assert_equal(first_time.id.to_s, response.headers['X-Workarea-Segments'])
53
+
54
+ get storefront_api.system_content_path('home_page'), params: { session_id: '2' }
55
+ assert_equal(returning.id.to_s, response.headers['X-Workarea-Segments'])
56
+ end
57
+
58
+ def test_products_active_by_segment
59
+ segment_one = create_segment(rules: [Segment::Rules::Sessions.new(minimum: 1, maximum: 1)])
60
+ segment_two = create_segment(rules: [Segment::Rules::Sessions.new(minimum: 2, maximum: 2)])
61
+ product_one = create_product(active: true, active_segment_ids: [segment_two.id])
62
+ product_two = create_product(active: true, active_segment_ids: [segment_one.id])
63
+
64
+ assert_raise InvalidDisplay do
65
+ get storefront_api.product_path(product_one), params: { sessions: 0 }
66
+ assert(response.not_found?)
67
+ end
68
+
69
+ assert_raise InvalidDisplay do
70
+ get storefront_api.product_path(product_two), params: { sessions: 0 }
71
+ assert(response.not_found?)
72
+ end
73
+
74
+ get storefront_api.search_path(q: '*'), params: { sessions: 0 }
75
+ assert(JSON.parse(response.body)['products'].empty?)
76
+
77
+ assert_raise InvalidDisplay do
78
+ get storefront_api.product_path(product_one), params: { sessions: 1 }
79
+ assert(response.not_found?)
80
+ end
81
+
82
+ get storefront_api.product_path(product_two), params: { sessions: 1 }
83
+ assert(response.ok?)
84
+
85
+ get storefront_api.search_path(q: '*'), params: { sessions: 1 }
86
+ products = JSON.parse(response.body)['products']
87
+ assert_equal([product_two.id], products.map { |p| p['id'] })
88
+
89
+ get storefront_api.product_path(product_one), params: { sessions: 2 }
90
+ assert(response.ok?)
91
+
92
+ assert_raise InvalidDisplay do
93
+ get storefront_api.product_path(product_two), params: { sessions: 2 }
94
+ assert(response.not_found?)
95
+ end
96
+
97
+ get storefront_api.search_path(q: '*'), params: { sessions: 2 }
98
+ products = JSON.parse(response.body)['products']
99
+ assert_equal([product_one.id], products.map { |p| p['id'] })
100
+
101
+ segment_one.rules.first.update!(minimum: 1, maximum: nil)
102
+ segment_two.rules.first.update!(minimum: 1, maximum: nil)
103
+
104
+ get storefront_api.product_path(product_one), params: { sessions: 1 }
105
+ assert(response.ok?)
106
+
107
+ get storefront_api.product_path(product_two), params: { sessions: 1 }
108
+ assert(response.ok?)
109
+
110
+ get storefront_api.search_path(q: '*'), params: { sessions: 1 }
111
+ products = JSON.parse(response.body)['products']
112
+ assert_equal(2, products.count)
113
+ assert_includes(products.map { |r| r['id'] }, product_one.id)
114
+ assert_includes(products.map { |r| r['id'] }, product_two.id)
115
+ end
116
+
117
+ def test_content_active_by_segment
118
+ segment_one = create_segment(rules: [Segment::Rules::Sessions.new(minimum: 1, maximum: 1)])
119
+ segment_two = create_segment(rules: [Segment::Rules::Sessions.new(minimum: 2, maximum: 2)])
120
+
121
+ content = Content.for('home_page')
122
+ foo = content.blocks.create!(
123
+ type: 'html',
124
+ data: { 'html' => '<p>Foo</p>' },
125
+ active_segment_ids: [segment_one.id]
126
+ )
127
+ bar = content.blocks.create!(
128
+ type: 'html',
129
+ data: { 'html' => '<p>Bar</p>' },
130
+ active_segment_ids: [segment_two.id]
131
+ )
132
+
133
+ get storefront_api.system_content_path('home_page'), params: { sessions: 1 }
134
+ content_blocks = JSON.parse(response.body)['content_blocks']
135
+ assert_equal([foo.id.to_s], content_blocks.map { |cb| cb['id'] })
136
+
137
+ get storefront_api.system_content_path('home_page'), params: { sessions: 2 }
138
+ content_blocks = JSON.parse(response.body)['content_blocks']
139
+ assert_equal([bar.id.to_s], content_blocks.map { |cb| cb['id'] })
140
+
141
+ segment_one.rules.first.update!(minimum: 1, maximum: nil)
142
+ segment_two.rules.first.update!(minimum: 1, maximum: nil)
143
+
144
+ get storefront_api.system_content_path('home_page'), params: { sessions: 1 }
145
+ content_blocks = JSON.parse(response.body)['content_blocks']
146
+ assert_equal([foo.id.to_s, bar.id.to_s], content_blocks.map { |cb| cb['id'] })
147
+ end
148
+
149
+ def test_logged_in_based_segments
150
+ logged_in = create_segment(rules: [Segment::Rules::LoggedIn.new(logged_in: true)])
151
+ logged_out = create_segment(rules: [Segment::Rules::LoggedIn.new(logged_in: false)])
152
+
153
+ get storefront_api.system_content_path('home_page')
154
+ assert_equal(logged_out.id.to_s, response.headers['X-Workarea-Segments'])
155
+
156
+ user = create_user
157
+ post storefront_api.authentication_tokens_path,
158
+ params: { email: user.email, password: user.password }
159
+
160
+ token = JSON.parse(response.body)['token']
161
+ get storefront_api.system_content_path('home_page'),
162
+ headers: { 'HTTP_AUTHORIZATION' => encode_credentials(token) }
163
+ assert_equal(logged_in.id.to_s, response.headers['X-Workarea-Segments'])
164
+ end
165
+
166
+ def test_http_caching_headers_for_segmented_content
167
+ Workarea.config.strip_http_caching_in_tests = false
168
+ segment = create_segment(rules: [Segment::Rules::Sessions.new(maximum: 999)])
169
+
170
+ get storefront_api.system_content_path('home_page')
171
+ refute_match(/private/, response.headers['Cache-Control'])
172
+ assert_match(/public/, response.headers['Cache-Control'])
173
+ assert(response.headers['X-Workarea-Segmented-Content'].blank?)
174
+
175
+ content = Content.for('home_page')
176
+ content.blocks.create!(
177
+ type: 'html',
178
+ data: { 'html' => '<p>Foo</p>' },
179
+ active_segment_ids: [segment.id]
180
+ )
181
+
182
+ get storefront_api.system_content_path('home_page')
183
+ assert_match(/private/, response.headers['Cache-Control'])
184
+ refute_match(/public/, response.headers['Cache-Control'])
185
+ assert_equal('true', response.headers['X-Workarea-Segmented-Content'])
186
+
187
+ product = create_product(active: true, active_segment_ids: [])
188
+ get storefront_api.product_path(product)
189
+ assert(response.ok?)
190
+ refute_match(/private/, response.headers['Cache-Control'])
191
+ assert_match(/public/, response.headers['Cache-Control'])
192
+ assert(response.headers['X-Workarea-Segmented-Content'].blank?)
193
+
194
+ product.update!(active_segment_ids: [segment.id])
195
+ get storefront_api.product_path(product)
196
+ assert(response.ok?)
197
+ assert_match(/private/, response.headers['Cache-Control'])
198
+ refute_match(/public/, response.headers['Cache-Control'])
199
+ assert_equal('true', response.headers['X-Workarea-Segmented-Content'])
200
+
201
+ category = create_category(active: true, active_segment_ids: [])
202
+ get storefront_api.category_path(category)
203
+ assert(response.ok?)
204
+ refute_match(/private/, response.headers['Cache-Control'])
205
+ assert_match(/public/, response.headers['Cache-Control'])
206
+ assert(response.headers['X-Workarea-Segmented-Content'].blank?)
207
+
208
+ category.update!(product_ids: [product.id])
209
+ get storefront_api.category_path(category)
210
+ assert(response.ok?)
211
+ assert_match(/private/, response.headers['Cache-Control'])
212
+ refute_match(/public/, response.headers['Cache-Control'])
213
+ assert_equal('true', response.headers['X-Workarea-Segmented-Content'])
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workarea-api-storefront
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.7
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Crouse
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-22 00:00:00.000000000 Z
11
+ date: 2019-11-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: workarea
@@ -81,7 +81,6 @@ files:
81
81
  - app/controllers/workarea/api/storefront/searches_controller.rb
82
82
  - app/controllers/workarea/api/storefront/system_content_controller.rb
83
83
  - app/controllers/workarea/api/storefront/taxons_controller.rb
84
- - app/controllers/workarea/api/storefront/user_activity.rb
85
84
  - app/controllers/workarea/storefront/application_controller.decorator
86
85
  - app/helpers/workarea/api/storefront/application_helper.rb
87
86
  - app/helpers/workarea/api/storefront/checkouts_helper.rb
@@ -89,7 +88,6 @@ files:
89
88
  - app/models/workarea/catalog/category.decorator
90
89
  - app/models/workarea/user.decorator
91
90
  - app/models/workarea/user/authentication_token.rb
92
- - app/view_models/workarea/api/storefront/search_suggestion_view_model.rb
93
91
  - app/views/workarea/api/storefront/accounts/_account.json.jbuilder
94
92
  - app/views/workarea/api/storefront/accounts/create.json.jbuilder
95
93
  - app/views/workarea/api/storefront/accounts/show.json.jbuilder
@@ -149,6 +147,7 @@ files:
149
147
  - config/routes.rb
150
148
  - lib/workarea/api/storefront.rb
151
149
  - lib/workarea/api/storefront/engine.rb
150
+ - lib/workarea/api/storefront/visit.decorator
152
151
  - test/documentation/workarea/api/storefront/accounts_documentation_test.rb
153
152
  - test/documentation/workarea/api/storefront/analytics_documentation_test.rb
154
153
  - test/documentation/workarea/api/storefront/assets_documentation_test.rb
@@ -169,6 +168,7 @@ files:
169
168
  - test/documentation/workarea/api/storefront/saved_addresses_documentation_test.rb
170
169
  - test/documentation/workarea/api/storefront/saved_credit_cards_documentation_test.rb
171
170
  - test/documentation/workarea/api/storefront/searches_documentation_test.rb
171
+ - test/documentation/workarea/api/storefront/segmentation_documentation_test.rb
172
172
  - test/documentation/workarea/api/storefront/system_content_documentation_test.rb
173
173
  - test/documentation/workarea/api/storefront/taxons_documentation_test.rb
174
174
  - test/documentation/workarea/api/storefront/validation_documentation_test.rb
@@ -246,6 +246,7 @@ files:
246
246
  - test/integration/workarea/api/storefront/saved_addresses_integration_test.rb
247
247
  - test/integration/workarea/api/storefront/saved_credit_cards_integration_test.rb
248
248
  - test/integration/workarea/api/storefront/searches_integration_test.rb
249
+ - test/integration/workarea/api/storefront/segments_integration_test.rb
249
250
  - test/integration/workarea/api/storefront/system_content_integration_test.rb
250
251
  - test/integration/workarea/api/storefront/taxons_integration_test.rb
251
252
  - test/integration/workarea/api/storefront/user_carts_integration_test.rb
@@ -273,7 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
273
274
  - !ruby/object:Gem::Version
274
275
  version: '0'
275
276
  requirements: []
276
- rubygems_version: 3.0.4
277
+ rubygems_version: 3.0.6
277
278
  signing_key:
278
279
  specification_version: 4
279
280
  summary: Storefront JSON REST API for the Workarea Commerce Platform
@@ -1,36 +0,0 @@
1
- module Workarea
2
- module Api
3
- module Storefront
4
- module UserActivity
5
- extend ActiveSupport::Concern
6
-
7
- def user_activity
8
- @user_activity ||= Recommendation::UserActivity.find_or_initialize_by(
9
- id: current_user_activity_id
10
- )
11
- end
12
-
13
- def current_user_activity_id
14
- if authentication?
15
- current_user.id
16
- else
17
- params[:session_id]
18
- end
19
- end
20
-
21
- def assert_current_user_activity_id
22
- if current_user_activity_id.blank?
23
- render(
24
- json: {
25
- problem: t('workarea.api.storefront.recent_views.missing_id')
26
- },
27
- status: :unprocessable_entity
28
- )
29
-
30
- return false
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,21 +0,0 @@
1
- module Workarea
2
- module Api
3
- module Storefront
4
- class SearchSuggestionViewModel < Workarea::Storefront::SearchSuggestionViewModel
5
- def url
6
- helpers = Api::Storefront::Engine.routes.url_helpers
7
-
8
- if suggestion_type == 'product'
9
- helpers.product_path(product)
10
- elsif suggestion_type == 'search'
11
- helpers.search_path(q: name)
12
- elsif suggestion_type == 'category'
13
- helpers.category_path(source['slug'])
14
- elsif suggestion_type == 'page'
15
- helpers.page_path(source['slug'])
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end