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
@@ -0,0 +1,10 @@
1
+ # XXX: frozen_string_literal comment breaks on Spree 3.0
2
+
3
+ Deface::Override.new(
4
+ virtual_path: 'spree/admin/shared/sub_menu/_configuration',
5
+ name: 'add_gladly_admin_menu_links',
6
+ insert_bottom: "ul[data-hook='admin_configurations_sidebar_menu']",
7
+ # rubocop:disable Layout/LineLength
8
+ text: "<%= configurations_sidebar_menu_item(Spree.t('spree_gladly.settings'), edit_admin_gladly_settings_path) if can? :manage, Spree::Config %>"
9
+ # rubocop:enable Layout/LineLength
10
+ )
@@ -0,0 +1,27 @@
1
+ module Customer
2
+ class AddressPresenter
3
+ def initialize(address)
4
+ @address = address
5
+ end
6
+
7
+ def to_s
8
+ return nil if address.nil?
9
+
10
+ components = [
11
+ address.address1,
12
+ address.address2,
13
+ address.city,
14
+ address.state_text,
15
+ address.zipcode,
16
+ address.country.to_s
17
+ ]
18
+
19
+ components.reject(&:blank?)
20
+ .join(', ')
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :address
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ class BasicLookupPresenter
5
+ def initialize(resource:)
6
+ @resource = resource
7
+ @registered_customers = resource.registered_customers
8
+ @guest_customers = resource.guest_customers
9
+ end
10
+
11
+ def to_h
12
+ return [] if guest_customers.empty? && registered_customers.empty?
13
+
14
+ registered_presenter.concat(guest_presenter)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :resource, :registered_customers, :guest_customers
20
+
21
+ def registered_presenter
22
+ Customer::Registered::BasicPresenter.new(resource: registered_customers).to_h
23
+ end
24
+
25
+ def guest_presenter
26
+ Customer::Guest::BasicPresenter.new(resource: guest_customers).to_h
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ class DetailedLookupPresenter
5
+ include Spree::Core::Engine.routes.url_helpers
6
+ include Spree::BaseHelper
7
+
8
+ def initialize(resource:)
9
+ @resource = resource
10
+ end
11
+
12
+ def to_h
13
+ return [] unless resource.customer.present?
14
+
15
+ resource.guest ? guest_presenter : registered_presenter
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :resource
21
+
22
+ def registered_presenter
23
+ Customer::Registered::DetailedPresenter.new(resource: resource).to_h
24
+ end
25
+
26
+ def guest_presenter
27
+ Customer::Guest::DetailedPresenter.new(resource: resource).to_h
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Guest
5
+ class BasicPresenter
6
+ def initialize(resource:)
7
+ @resource = resource
8
+ end
9
+
10
+ def to_h
11
+ return [] if resource.empty?
12
+
13
+ basic_payload
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :resource
19
+
20
+ def basic_payload
21
+ resource.map do |guest_customer|
22
+ {
23
+ externalCustomerId: guest_customer.email,
24
+ address: formatted_address(guest_customer),
25
+ name: guest_customer&.bill_address&.full_name.to_s,
26
+ emails: customer_emails(guest_customer),
27
+ phones: customer_phones(guest_customer)
28
+ }
29
+ end
30
+ end
31
+
32
+ def customer_emails(guest_customer)
33
+ [
34
+ {
35
+ original: guest_customer.email
36
+ }
37
+ ]
38
+ end
39
+
40
+ def customer_phones(guest_customer)
41
+ [
42
+ {
43
+ original: guest_customer&.bill_address&.phone.to_s
44
+ }
45
+ ]
46
+ end
47
+
48
+ def formatted_address(guest_customer)
49
+ Customer::AddressPresenter.new(guest_customer&.bill_address).to_s
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Guest
5
+ class DetailedPresenter
6
+ include Spree::Core::Engine.routes.url_helpers
7
+ include Spree::BaseHelper
8
+
9
+ def initialize(resource:)
10
+ @resource = resource
11
+ end
12
+
13
+ def to_h
14
+ return [] unless resource.customer.present?
15
+
16
+ detailed_payload
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :resource
22
+
23
+ def detailed_payload
24
+ [
25
+ {
26
+ externalCustomerId: resource.customer.email,
27
+ customAttributes: custom_attributes,
28
+ emails: emails,
29
+ transactions: transactions
30
+ }
31
+ ]
32
+ end
33
+
34
+ def custom_attributes
35
+ {
36
+ totalOrderCount: transactions_size,
37
+ guestOrderCount: transactions_size,
38
+ lifetimeValue: lifetime_value
39
+ }
40
+ end
41
+
42
+ def transactions
43
+ resource.transactions.map do |transaction|
44
+ {
45
+ type: 'ORDER',
46
+ orderStatus: transaction.state,
47
+ orderNumber: transaction.number,
48
+ guest: 'yes',
49
+ products: transaction_products(transaction: transaction),
50
+ orderLink: order_url(transaction),
51
+ note: transaction&.special_instructions.to_s,
52
+ orderTotal: Spree::Money.new(transaction.total).to_html,
53
+ createdAt: transaction.created_at
54
+ }
55
+ end
56
+ end
57
+
58
+ def transaction_products(transaction:)
59
+ transaction.line_items.map do |item|
60
+ {
61
+ name: item.variant.name,
62
+ status: item_status(item: item),
63
+ sku: item.variant.sku,
64
+ quantity: item.quantity.to_s,
65
+ total: Spree::Money.new(item.total).to_html,
66
+ unitPrice: Spree::Money.new(item.price).to_html,
67
+ imageUrl: item_image_url(item: item)
68
+ }
69
+ end
70
+ end
71
+
72
+ def transactions_size
73
+ @transactions_size ||= resource.transactions.size.to_s
74
+ end
75
+
76
+ def lifetime_value
77
+ return '0' if resource.transactions.empty?
78
+
79
+ Spree::Money.new(resource.transactions.sum(&:total)).to_html
80
+ end
81
+
82
+ def order_url(transaction)
83
+ edit_admin_order_url(id: transaction.number, host: Rails.application.routes.default_url_options[:host])
84
+ end
85
+
86
+ def item_image_url(item:)
87
+ return '' if item.product.images.empty?
88
+
89
+ item.product.images.first&.attachment&.url
90
+ end
91
+
92
+ def item_status(item:)
93
+ item.sufficient_stock? ? 'fulfilled' : 'cancelled'
94
+ end
95
+
96
+ def emails
97
+ [
98
+ {
99
+ original: resource.customer.email
100
+ }
101
+ ]
102
+ end
103
+
104
+ def phones
105
+ [
106
+ {
107
+ original: address&.phone
108
+ }
109
+ ]
110
+ end
111
+
112
+ def address
113
+ @address ||= resource.customer.bill_address
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Customer
4
+ module Registered
5
+ class BasicPresenter
6
+ def initialize(resource:)
7
+ @resource = resource
8
+ end
9
+
10
+ def to_h
11
+ return [] if resource.empty?
12
+
13
+ basic_payload
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :resource
19
+
20
+ def basic_payload
21
+ resource.map do |user|
22
+ {
23
+ externalCustomerId: user.email,
24
+ customAttributes: {
25
+ spreeId: user.id
26
+ },
27
+ address: formatted_address(user),
28
+ name: address(user)&.full_name.to_s,
29
+ emails: customer_emails(user),
30
+ phones: customer_phones(user)
31
+ }
32
+ end
33
+ end
34
+
35
+ def customer_emails(user)
36
+ [
37
+ {
38
+ original: user.email
39
+ }
40
+ ]
41
+ end
42
+
43
+ def customer_phones(user)
44
+ [
45
+ {
46
+ original: address(user)&.phone.to_s
47
+ }
48
+ ]
49
+ end
50
+
51
+ def address(user)
52
+ user.bill_address || user.orders.first&.bill_address
53
+ end
54
+
55
+ def formatted_address(user)
56
+ Customer::AddressPresenter.new(address(user)).to_s
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ClassLength
4
+ module Customer
5
+ module Registered
6
+ class DetailedPresenter
7
+ include Spree::Core::Engine.routes.url_helpers
8
+ include Spree::BaseHelper
9
+
10
+ def initialize(resource:)
11
+ @resource = resource
12
+ end
13
+
14
+ def to_h
15
+ return [] unless resource.customer.present?
16
+
17
+ detailed_payload
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :resource
23
+
24
+ def detailed_payload
25
+ [
26
+ {
27
+ externalCustomerId: resource.customer.email,
28
+ name: address&.full_name,
29
+ address: formatted_address,
30
+ emails: emails,
31
+ phones: phones,
32
+ customAttributes: custom_attributes,
33
+ transactions: transactions
34
+ }
35
+ ]
36
+ end
37
+
38
+ def custom_attributes
39
+ {
40
+ spreeId: resource.customer.id,
41
+ lifetimeValue: lifetime_value,
42
+ totalOrderCount: resource.transactions.size.to_s,
43
+ guestOrderCount: calculate_guest_transactions.to_s,
44
+ memberSince: resource.customer.created_at,
45
+ customerLink: customer_profile_url(resource.customer)
46
+ }
47
+ end
48
+
49
+ def transactions
50
+ resource.transactions.map do |transaction|
51
+ {
52
+ type: 'ORDER',
53
+ orderStatus: transaction.state,
54
+ orderNumber: transaction.number,
55
+ guest: transaction_type(transaction),
56
+ products: transaction_products(transaction: transaction),
57
+ orderLink: order_url(transaction),
58
+ note: transaction&.special_instructions.to_s,
59
+ orderTotal: Spree::Money.new(transaction.total).to_html,
60
+ createdAt: transaction.created_at
61
+ }
62
+ end
63
+ end
64
+
65
+ def transaction_products(transaction:)
66
+ transaction.line_items.map do |item|
67
+ {
68
+ name: item.variant.name,
69
+ status: item_status(item: item),
70
+ sku: item.variant.sku,
71
+ quantity: item.quantity.to_s,
72
+ total: Spree::Money.new(item.total).to_html,
73
+ unitPrice: Spree::Money.new(item.price).to_html,
74
+ imageUrl: item_image_url(item: item)
75
+ }
76
+ end
77
+ end
78
+
79
+ def calculate_guest_transactions
80
+ resource.transactions.select { |item| item.user_id.nil? }.size
81
+ end
82
+
83
+ def transaction_type(order)
84
+ order.user_id.nil? ? 'yes' : 'no'
85
+ end
86
+
87
+ def customer_profile_url(customer)
88
+ edit_admin_user_url(id: customer.id, host: Rails.application.routes.default_url_options[:host])
89
+ end
90
+
91
+ def order_url(transaction)
92
+ edit_admin_order_url(id: transaction.number, host: Rails.application.routes.default_url_options[:host])
93
+ end
94
+
95
+ def lifetime_value
96
+ return '0' if resource.transactions.empty?
97
+
98
+ Spree::Money.new(resource.transactions.sum(&:total)).to_html
99
+ end
100
+
101
+ def item_image_url(item:)
102
+ return '' if item.product.images.empty?
103
+
104
+ item.product.images.first&.attachment&.url
105
+ end
106
+
107
+ def item_status(item:)
108
+ item.sufficient_stock? ? 'fulfilled' : 'cancelled'
109
+ end
110
+
111
+ def emails
112
+ [
113
+ {
114
+ original: resource.customer.email
115
+ }
116
+ ]
117
+ end
118
+
119
+ def phones
120
+ [
121
+ {
122
+ original: address&.phone
123
+ }
124
+ ]
125
+ end
126
+
127
+ def address
128
+ @address ||= resource.customer.bill_address || resource.transactions.first.bill_address
129
+ end
130
+
131
+ def formatted_address
132
+ Customer::AddressPresenter.new(address).to_s
133
+ end
134
+ end
135
+ end
136
+ end
137
+ # rubocop:enable Metrics/ClassLength