spree_gladly 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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