spree_gladly 1.0.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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +45 -0
  5. data/.travis.yml +69 -0
  6. data/Appraisals +63 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/Gemfile +7 -0
  9. data/LICENSE +11 -0
  10. data/README.md +473 -0
  11. data/Rakefile +23 -0
  12. data/app/concerns/customer/database_adapter.rb +21 -0
  13. data/app/controllers/application_controller.rb +2 -0
  14. data/app/controllers/spree/admin/gladly_settings_controller.rb +38 -0
  15. data/app/controllers/spree/api/v1/customers_controller.rb +60 -0
  16. data/app/finders/customer/base_lookup.rb +37 -0
  17. data/app/finders/customer/basic_lookup.rb +26 -0
  18. data/app/finders/customer/detailed_lookup.rb +19 -0
  19. data/app/finders/customer/guest/basic_finder.rb +37 -0
  20. data/app/finders/customer/guest/detailed_finder.rb +42 -0
  21. data/app/finders/customer/registered/basic_finder.rb +68 -0
  22. data/app/finders/customer/registered/detailed_finder.rb +43 -0
  23. data/app/models/spree_gladly/configuration.rb +25 -0
  24. data/app/overrides/add_gladly_admin_menu_links.rb +10 -0
  25. data/app/presenters/customer/address_presenter.rb +27 -0
  26. data/app/presenters/customer/basic_lookup_presenter.rb +29 -0
  27. data/app/presenters/customer/detailed_lookup_presenter.rb +30 -0
  28. data/app/presenters/customer/guest/basic_presenter.rb +53 -0
  29. data/app/presenters/customer/guest/detailed_presenter.rb +117 -0
  30. data/app/presenters/customer/registered/basic_presenter.rb +60 -0
  31. data/app/presenters/customer/registered/detailed_presenter.rb +137 -0
  32. data/app/services/auth/authorization_header.rb +35 -0
  33. data/app/services/auth/error.rb +4 -0
  34. data/app/services/auth/header_parse_error.rb +4 -0
  35. data/app/services/auth/invalid_signature_error.rb +4 -0
  36. data/app/services/auth/missing_key_error.rb +4 -0
  37. data/app/services/auth/request_normalizer.rb +37 -0
  38. data/app/services/auth/signature_validator.rb +68 -0
  39. data/app/services/auth/time_header.rb +25 -0
  40. data/app/validators/lookup_validator.rb +89 -0
  41. data/app/validators/validation_result.rb +25 -0
  42. data/app/views/spree/admin/gladly_settings/edit.html.erb +25 -0
  43. data/bin/console +15 -0
  44. data/bin/setup +8 -0
  45. data/config/locales/en.yml +25 -0
  46. data/config/routes.rb +13 -0
  47. data/gemfiles/spree_3_0.gemfile +16 -0
  48. data/gemfiles/spree_3_1.gemfile +16 -0
  49. data/gemfiles/spree_3_7.gemfile +11 -0
  50. data/gemfiles/spree_4_0.gemfile +11 -0
  51. data/gemfiles/spree_4_1.gemfile +11 -0
  52. data/gemfiles/spree_4_2.gemfile +11 -0
  53. data/gemfiles/spree_master.gemfile +11 -0
  54. data/lib/generators/spree_gladly/install/install_generator.rb +17 -0
  55. data/lib/generators/spree_gladly/install/templates/config/initializers/spree_gladly.rb +18 -0
  56. data/lib/spree_gladly.rb +13 -0
  57. data/lib/spree_gladly/engine.rb +25 -0
  58. data/lib/spree_gladly/factories.rb +9 -0
  59. data/lib/spree_gladly/version.rb +5 -0
  60. data/spree.png +0 -0
  61. data/spree_gladly.gemspec +35 -0
  62. metadata +201 -0
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rspec/core/rake_task'
7
+ require 'spree/testing_support/extension_rake'
8
+
9
+ RSpec::Core::RakeTask.new
10
+
11
+ task :default do
12
+ if Dir['spec/dummy'].empty?
13
+ Rake::Task[:test_app].invoke
14
+ Dir.chdir('../../')
15
+ end
16
+ Rake::Task[:spec].invoke
17
+ end
18
+
19
+ desc 'Generates a dummy app for testing'
20
+ task :test_app do
21
+ ENV['LIB_NAME'] = 'spree_gladly'
22
+ Rake::Task['extension:test_app'].invoke
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module DatabaseAdapter
5
+ def concat(*args)
6
+ if adapter =~ /mysql/i
7
+ "CONCAT(#{args.join(',')})"
8
+ else
9
+ args.join('||')
10
+ end
11
+ end
12
+
13
+ def adapter
14
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
15
+ ActiveRecord::Base.connection_db_config.configuration_hash[:adapter]
16
+ else
17
+ ActiveRecord::Base.connection_config[:adapter]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Admin
5
+ class GladlySettingsController < ::Spree::Admin::BaseController
6
+ NONNEGATIVE_INT_REGEX = /\A[0-9]+\Z/.freeze
7
+
8
+ def edit
9
+ @signing_key = SpreeGladly::Config.signing_key
10
+ @signing_threshold = SpreeGladly::Config.signing_threshold
11
+ end
12
+
13
+ def update
14
+ if params[:signing_threshold].present? && params[:signing_threshold] !~ NONNEGATIVE_INT_REGEX
15
+ flash[:error] = Spree.t('spree_gladly.signing_threshold_error')
16
+ else
17
+ set_signing_key
18
+ set_signing_threshold
19
+ flash[:success] = Spree.t('spree_gladly.save_success')
20
+ end
21
+
22
+ redirect_to edit_admin_gladly_settings_path
23
+ end
24
+
25
+ private
26
+
27
+ def set_signing_key
28
+ SpreeGladly::Config.signing_key = params[:signing_key] if params.key?('signing_key')
29
+ end
30
+
31
+ def set_signing_threshold
32
+ return unless params.key?(:signing_threshold)
33
+
34
+ SpreeGladly::Config.signing_threshold = [0, params[:signing_threshold].to_i].max
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V1
6
+ class CustomersController < ::ApplicationController
7
+ skip_before_action :verify_authenticity_token, only: :lookup
8
+ before_action :validate_signature, only: :lookup
9
+ before_action :validate_params, only: :lookup
10
+
11
+ rescue_from ::Auth::InvalidSignatureError, with: :authorization_error
12
+ rescue_from ::Auth::MissingKeyError, with: :authorization_error
13
+ rescue_from ::Auth::HeaderParseError, with: :authorization_error
14
+
15
+ def lookup
16
+ lookup_level = params['lookupLevel'].downcase.to_sym
17
+ collection = customer_lookup(type: lookup_level).execute
18
+
19
+ render json: serialize_collection(
20
+ type: lookup_level,
21
+ collection: collection
22
+ ), status: 200
23
+ end
24
+
25
+ private
26
+
27
+ def serialize_collection(type:, collection:)
28
+ presenter = {
29
+ detailed: SpreeGladly::Config.detailed_lookup_presenter.new(resource: collection),
30
+ basic: SpreeGladly::Config.basic_lookup_presenter.new(resource: collection)
31
+ }[type]
32
+
33
+ { results: presenter.to_h }
34
+ end
35
+
36
+ def customer_lookup(type:)
37
+ {
38
+ detailed: Customer::DetailedLookup.new(params: params),
39
+ basic: Customer::BasicLookup.new(params: params)
40
+ }[type]
41
+ end
42
+
43
+ def validate_signature
44
+ ::Auth::SignatureValidator.new(SpreeGladly::Config.signing_key,
45
+ SpreeGladly::Config.signing_threshold).validate(request)
46
+ end
47
+
48
+ def authorization_error(error)
49
+ errors = [{ attr: 'Gladly-Authorization', code: error.class.to_s, detail: error.to_s }]
50
+ render json: { errors: errors }, status: 401
51
+ end
52
+
53
+ def validate_params
54
+ result = LookupValidator.new.call(params.permit!.to_h.deep_symbolize_keys)
55
+ render json: { errors: result.format_errors }, status: 422 unless result.success?
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ class BaseLookup
5
+ include Customer::DatabaseAdapter
6
+
7
+ def initialize(params:)
8
+ @params = params
9
+ @query = params.include?(:query) ? params.fetch(:query) : {}
10
+
11
+ @emails = normalize_param(param: query[:emails])
12
+ @phones = normalize_param(param: query[:phones])
13
+ @name = query[:name]
14
+ @external_customer_id = query[:externalCustomerId]
15
+ @spree_id = query[:spreeId]
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :params, :query, :emails, :phones, :name, :external_customer_id, :spree_id
21
+
22
+ def customer
23
+ @customer ||= Spree.user_class.where('id = ? OR email = ?', spree_id.to_i, external_customer_id).take
24
+ end
25
+
26
+ def guest_customer?
27
+ !customer.present?
28
+ end
29
+
30
+ def normalize_param(param:)
31
+ return [] if param.nil?
32
+ return param if param.is_a?(Array)
33
+
34
+ param.split(',').map(&:strip)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ class BasicLookup < Customer::BaseLookup
5
+ def execute
6
+ OpenStruct.new(
7
+ guest_customers: guest_customers(registered_customers.pluck(:email)),
8
+ registered_customers: registered_customers.uniq.sort
9
+ )
10
+ end
11
+
12
+ private
13
+
14
+ def guest_customers(excluded_emails)
15
+ Customer::Guest::BasicFinder.new(emails: emails, options: { excluded_emails: excluded_emails }).execute
16
+ end
17
+
18
+ def registered_customers
19
+ @registered_customers ||= Customer::Registered::BasicFinder.new(
20
+ name: name,
21
+ emails: emails,
22
+ phones: phones
23
+ ).execute
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ class DetailedLookup < Customer::BaseLookup
5
+ def execute
6
+ guest_customer? ? guest_customer : registered_customer
7
+ end
8
+
9
+ private
10
+
11
+ def guest_customer
12
+ Customer::Guest::DetailedFinder.new(email: external_customer_id).execute
13
+ end
14
+
15
+ def registered_customer
16
+ Customer::Registered::DetailedFinder.new(customer: customer).execute
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Guest
5
+ class BasicFinder
6
+ def initialize(emails:, options: {})
7
+ @emails = emails
8
+ @options = options
9
+ end
10
+
11
+ def execute
12
+ return [] if emails.empty?
13
+
14
+ guest_customers
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :emails, :options
20
+
21
+ def guest_customers
22
+ Spree::Order
23
+ .where(user_id: nil)
24
+ .where(email: search_emails)
25
+ .order(created_at: :desc)
26
+ .to_a
27
+ .uniq(&:email)
28
+ end
29
+
30
+ def search_emails
31
+ return emails - options[:excluded_emails] if options[:excluded_emails].present?
32
+
33
+ emails
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Guest
5
+ class DetailedFinder
6
+ def initialize(email:)
7
+ @email = email
8
+ end
9
+
10
+ def execute
11
+ OpenStruct.new(customer: customer, transactions: transactions, guest: true)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :email
17
+
18
+ def customer
19
+ transactions.first || []
20
+ end
21
+
22
+ def transactions
23
+ @transactions ||= find_transactions
24
+ end
25
+
26
+ def find_transactions
27
+ scope = Spree::Order
28
+ .includes(SpreeGladly::Config.order_includes)
29
+ .where(state: SpreeGladly::Config.order_states)
30
+ .order(SpreeGladly::Config.order_sorting)
31
+ .where("(#{order_table}.user_id IS NULL AND #{order_table}.email = ?)", email)
32
+
33
+ scope = scope.limit(SpreeGladly::Config.order_limit) if SpreeGladly::Config.order_limit
34
+ scope.to_a
35
+ end
36
+
37
+ def order_table
38
+ Spree::Order.table_name
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Registered
5
+ class BasicFinder
6
+ include Customer::DatabaseAdapter
7
+
8
+ def initialize(name:, emails:, phones:)
9
+ @name = name
10
+ @emails = emails
11
+ @phones = phones
12
+ end
13
+
14
+ def execute
15
+ registered_customers
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :name, :emails, :phones
21
+
22
+ def registered_customers
23
+ conditions = search_conditions
24
+ return empty_scope unless conditions.present?
25
+
26
+ template = conditions.map(&:first).join(' OR ')
27
+ args = conditions.map(&:last)
28
+
29
+ scope.where(template, *args)
30
+ end
31
+
32
+ def search_conditions
33
+ [by_email, by_name, by_phone].compact
34
+ end
35
+
36
+ def by_email
37
+ return nil unless emails.present?
38
+
39
+ where = "#{Spree.user_class.table_name}.email IN (?)"
40
+ [where, emails]
41
+ end
42
+
43
+ def by_name
44
+ return nil unless name.present?
45
+
46
+ sql_name = concat("#{Spree::Address.table_name}.firstname", "' '", "#{Spree::Address.table_name}.lastname")
47
+ where = "(LOWER(#{sql_name}) LIKE ?)"
48
+ args = "%#{name.downcase}%"
49
+ [where, args]
50
+ end
51
+
52
+ def by_phone
53
+ return nil unless phones.present?
54
+
55
+ where = "#{Spree::Address.table_name}.phone IN (?)"
56
+ [where, phones]
57
+ end
58
+
59
+ def empty_scope
60
+ Spree.user_class.none
61
+ end
62
+
63
+ def scope
64
+ Spree.user_class.eager_load(:bill_address, :orders)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Registered
5
+ class DetailedFinder
6
+ def initialize(customer:)
7
+ @customer = customer
8
+ end
9
+
10
+ def execute
11
+ return empty_result if customer.nil?
12
+
13
+ OpenStruct.new(customer: customer, transactions: transactions, guest: false)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :customer
19
+
20
+ def transactions
21
+ customer_orders = "(#{order_table}.user_id = ?)"
22
+ guest_orders = "(#{order_table}.user_id IS NULL AND #{order_table}.email = ?)"
23
+
24
+ scope = Spree::Order
25
+ .includes(SpreeGladly::Config.order_includes)
26
+ .where(state: SpreeGladly::Config.order_states)
27
+ .order(SpreeGladly::Config.order_sorting)
28
+ .where("#{customer_orders} OR #{guest_orders}", customer.id, customer.email)
29
+
30
+ scope = scope.limit(SpreeGladly::Config.order_limit) if SpreeGladly::Config.order_limit
31
+ scope.to_a
32
+ end
33
+
34
+ def empty_result
35
+ OpenStruct.new(customer: [], transactions: [], guest: false)
36
+ end
37
+
38
+ def order_table
39
+ Spree::Order.table_name
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ module SpreeGladly
2
+ class Configuration < ::Spree::Preferences::Configuration
3
+ preference :signing_key, :string, default: ''
4
+ preference :signing_threshold, :integer, default: 0
5
+
6
+ attr_accessor :basic_lookup_presenter,
7
+ :detailed_lookup_presenter,
8
+ :order_limit,
9
+ :order_includes,
10
+ :order_sorting,
11
+ :order_states
12
+
13
+ @basic_lookup_presenter = nil
14
+
15
+ @detailed_lookup_presenter = nil
16
+
17
+ @order_limit = nil
18
+
19
+ @order_includes = nil
20
+
21
+ @order_sorting = nil
22
+
23
+ @order_states = nil
24
+ end
25
+ end