flowcommerce_spree 0.0.1 → 0.0.6

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -24
  3. data/SPREE_FLOW.md +6 -28
  4. data/app/controllers/concerns/current_zone_loader_decorator.rb +40 -25
  5. data/app/controllers/flowcommerce_spree/inventory_controller.rb +23 -0
  6. data/app/controllers/flowcommerce_spree/orders_controller.rb +20 -0
  7. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +23 -13
  8. data/app/controllers/users/sessions_controller_decorator.rb +28 -0
  9. data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +4 -9
  10. data/app/models/spree/address_decorator.rb +19 -0
  11. data/app/models/spree/app_configuration_decorator.rb +7 -0
  12. data/app/models/spree/calculator/flow_io.rb +61 -0
  13. data/app/models/spree/calculator/shipping/flow_io.rb +40 -0
  14. data/app/models/spree/flow_io_credit_card_decorator.rb +21 -0
  15. data/app/models/spree/flow_io_order_decorator.rb +163 -0
  16. data/app/models/spree/flow_io_product_decorator.rb +2 -2
  17. data/app/models/spree/flow_io_variant_decorator.rb +4 -2
  18. data/app/models/spree/gateway/flow_io.rb +153 -0
  19. data/app/models/spree/{credit_card_decorator.rb → payment_capture_event_decorator.rb} +1 -1
  20. data/app/models/spree/promotion_handler/coupon_decorator.rb +1 -1
  21. data/app/models/spree/zones/flow_io_product_zone_decorator.rb +8 -0
  22. data/app/models/tracking/setup_decorator.rb +40 -0
  23. data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
  24. data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
  25. data/app/serializers/api/v2/order_serializer_decorator.rb +20 -0
  26. data/app/services/flowcommerce_spree/import_experience_items.rb +1 -1
  27. data/app/services/flowcommerce_spree/order_sync.rb +81 -173
  28. data/app/services/flowcommerce_spree/order_updater.rb +78 -0
  29. data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
  30. data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
  31. data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
  32. data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
  33. data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
  34. data/app/views/spree/admin/payments/source_views/_flow_io_gateway.html.erb +21 -0
  35. data/config/rails_best_practices.yml +51 -0
  36. data/config/routes.rb +3 -1
  37. data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +6 -4
  38. data/lib/flow/simple_gateway.rb +0 -36
  39. data/lib/flowcommerce_spree.rb +17 -3
  40. data/lib/flowcommerce_spree/engine.rb +33 -3
  41. data/lib/flowcommerce_spree/experience_service.rb +1 -27
  42. data/lib/flowcommerce_spree/logging_http_client.rb +33 -15
  43. data/lib/flowcommerce_spree/session.rb +16 -29
  44. data/lib/flowcommerce_spree/test_support.rb +7 -0
  45. data/lib/flowcommerce_spree/version.rb +1 -1
  46. data/lib/tasks/flowcommerce_spree.rake +4 -1
  47. metadata +90 -22
  48. data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
  49. data/app/models/spree/gateway/spree_flow_gateway.rb +0 -116
  50. data/app/models/spree/line_item_decorator.rb +0 -15
  51. data/app/models/spree/order_decorator.rb +0 -179
  52. data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
  53. data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
  54. data/config/initializers/flowcommerce_spree.rb +0 -7
  55. data/lib/flow/error.rb +0 -73
  56. data/lib/flow/pay_pal.rb +0 -25
  57. data/lib/flowcommerce_spree/webhook_service.rb +0 -98
  58. data/lib/simple_csv_writer.rb +0 -44
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spree
4
- CreditCard.class_eval do
4
+ PaymentCaptureEvent.class_eval do
5
5
  serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
6
6
 
7
7
  store_accessor :meta, :flow_data
@@ -5,7 +5,7 @@ module Spree
5
5
  Coupon.class_eval do
6
6
  def apply
7
7
  if order.coupon_code.present?
8
- if promotion&.actions.exists?
8
+ if promotion&.actions&.exists?
9
9
  experience_key = order.flow_order&.dig('experience', 'key')
10
10
  forbiden_keys = promotion.flow_data&.dig('filter', 'experience') || []
11
11
 
@@ -16,6 +16,10 @@ module Spree
16
16
  flow_data&.[]('key')
17
17
  end
18
18
 
19
+ def flow_io_experience_country
20
+ flow_data&.[]('country')
21
+ end
22
+
19
23
  def flow_io_experience_currency
20
24
  flow_data&.[]('currency')
21
25
  end
@@ -24,6 +28,10 @@ module Spree
24
28
  flow_data&.[]('key').present? && flow_data['status'] == 'active'
25
29
  end
26
30
 
31
+ def flow_io_active_or_archiving_experience?
32
+ flow_data&.[]('key').present? && %w[active archiving].include?(flow_data['status'])
33
+ end
34
+
27
35
  def update_on_flow; end
28
36
 
29
37
  def remove_on_flow_io
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracking
4
+ Setup.module_eval do
5
+ private
6
+
7
+ def setup_tracking
8
+ return if request.path.start_with?(ADMIN_PATH)
9
+
10
+ user_consents = UserConsent.new(cookies)
11
+ setup_visitor_cookie(user_consents)
12
+ store_order_flow_io_attributes(user_consents) if current_order&.zone&.flow_io_active_experience?
13
+ end
14
+
15
+ def store_order_flow_io_attributes(user_consents)
16
+ # Using `save!` and not `update_column` for callbacks to work and sync the order to flow.io
17
+ current_order.save!(validate: false) if order_user_consents_updated?(user_consents) || user_uuid_updated?
18
+ end
19
+
20
+ def order_user_consents_updated?(user_consents)
21
+ consents_changed = nil
22
+ user_consents.active_groups.each do |consent_group|
23
+ group_value = consent_group[1][:value]
24
+ gdpr_group_name = "gdpr_#{consent_group[1][:name]}"
25
+ next if current_order.flow_io_attributes[gdpr_group_name] == group_value
26
+
27
+ consents_changed ||= true
28
+ current_order.flow_io_attribute_add(gdpr_group_name, group_value)
29
+ end
30
+
31
+ consents_changed
32
+ end
33
+
34
+ def user_uuid_updated?
35
+ return if current_order.flow_io_attr_user_uuid.present?
36
+
37
+ current_order.add_user_uuid_to_flow_data
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/shared/_order_tabs',
3
+ name: 'spree_admin_order_additional_information_flow_message',
4
+ insert_top: '.additional-info',
5
+ text: '
6
+ <% if FlowcommerceSpree::ORGANIZATION.present? && @order.flow_order.present? %>
7
+ <div style="text-align: center">
8
+ <%= link_to "See Flow Order",
9
+ "https://console.flow.io/#{FlowcommerceSpree::ORGANIZATION}/orders/#{@order.number}",
10
+ target: "_blank", class: "button" %>
11
+ </div>
12
+ <% end %>'
13
+ )
@@ -0,0 +1,9 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/prices/index',
3
+ name: 'spree_admin_prices_flow_mesage',
4
+ insert_top: '.no-border-top',
5
+ text: "
6
+ <div class='spree-admin-info' >
7
+ To check localized pricing, please click <a href='#{"https://console.flow.io/#{ENV['FLOW_ORGANIZATION']}/price-books"}' target='_blank'>here</a>.
8
+ </div>"
9
+ )
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ Api::V2::OrderSerializer.class_eval do
4
+ attribute :duty_included, if: proc { object.flow_io_attributes.present? }
5
+ attribute :vat_included, if: proc { object.flow_io_attributes.present? }
6
+
7
+ def duty_included
8
+ flow_io_order_attributes&.[]('duty') == 'included'
9
+ end
10
+
11
+ def vat_included
12
+ flow_io_order_attributes&.[]('vat') == 'included'
13
+ end
14
+
15
+ private
16
+
17
+ def flow_io_order_attributes
18
+ @flow_io_order_attributes ||= Oj.load(object.flow_io_attributes['pricing_key'])
19
+ end
20
+ end
@@ -13,7 +13,7 @@ module FlowcommerceSpree
13
13
  items = []
14
14
  total = 0
15
15
 
16
- while offset == 0 || items.length == 100
16
+ while offset == 0 || items.length != 0
17
17
  # show current list size
18
18
  @logger.info "\nGetting items: #{@experience_key.green}, rows #{offset} - #{offset + page_size}"
19
19
 
@@ -2,196 +2,118 @@
2
2
 
3
3
  module FlowcommerceSpree
4
4
  # represents flow.io order syncing service
5
- # for easy integration we are currently passing:
6
- # - flow experience
7
- # - spree order
8
- # - current customer, present as @current_spree_user controller instance variable
9
- #
10
- # example:
11
- # flow_order = FlowcommerceSpree::OrderSync.new # init flow-order object
12
- # order: Spree::Order.last,
13
- # experience: @flow_session.experience
14
- # customer: Spree::User.last
15
- # flow_order.build_flow_request # builds json body to be posted to flow.io api
16
- # flow_order.synchronize! # sends order to flow
17
5
  class OrderSync
18
6
  FLOW_CENTER = 'default'
19
7
 
20
- attr_reader :digest, :order, :response
8
+ attr_reader :order, :response
21
9
 
22
- class << self
23
- def clear_cache(order)
24
- return unless order.flow_data['order']
10
+ # @param [Object] order
11
+ # @param [String] flow_session_id
12
+ def initialize(order:, flow_session_id:)
13
+ raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
25
14
 
26
- order.flow_data.delete('order')
27
- order.update_column :meta, order.meta.to_json
28
- end
29
- end
30
-
31
- def initialize(order:)
32
- raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
33
-
34
- @client = FlowcommerceSpree.client(session_id: order.flow_data['session_id'])
35
15
  @experience = order.flow_io_experience_key
36
- @order = order
37
- @customer = order.user
38
- @items = []
16
+ @flow_session_id = flow_session_id
17
+ @order = order
18
+ @client = FlowcommerceSpree.client(default_headers: { "Authorization": "Session #{flow_session_id}" },
19
+ authorization: nil)
39
20
  end
40
21
 
41
22
  # helper method to send complete order from Spree to flow.io
42
23
  def synchronize!
43
- sync_body!
44
- check_state!
45
- write_response_in_cache
46
- @response
47
- end
24
+ return unless @order.state == 'cart' && @order.line_items.size > 0
48
25
 
49
- def error
50
- @response['messages'].join(', ')
51
- end
26
+ sync_body!
27
+ write_response_to_order
52
28
 
53
- def error_code
54
- @response['code']
29
+ @order.update_columns(total: @order.total, meta: @order.meta.to_json)
30
+ refresh_checkout_token
55
31
  end
56
32
 
57
33
  def error?
58
34
  @response&.[]('code') && @response&.[]('messages') ? true : false
59
35
  end
60
36
 
61
- def delivery
62
- deliveries.select { |el| el[:active] }.first
63
- end
64
-
65
- # delivery methods are defined in flow console
66
- def deliveries
67
- # if we have erorr with an order, but still using this method
68
- return [] unless @order.flow_order
69
-
70
- @order.flow_data ||= {}
71
-
72
- delivery_list = @order.flow_order['deliveries'][0]['options']
73
- delivery_list = delivery_list.map do |opts|
74
- name = opts['tier']['name']
75
-
76
- # add original Flow ID
77
- # name += ' (%s)' % opts['tier']['strategy'] if opts['tier']['strategy']
78
-
79
- selection_id = opts['id']
80
-
81
- { id: selection_id,
82
- price: { label: opts['price']['label'] },
83
- active: @order.flow_order['selections'].include?(selection_id),
84
- name: name }
85
- end.to_a
86
-
87
- # make first one active unless we have active element
88
- delivery_list.first[:active] = true unless delivery_list.select { |el| el[:active] }.first
89
-
90
- delivery_list
91
- end
92
-
93
- def total_price
94
- @order.flow_total
95
- end
96
-
97
- def delivered_duty
98
- # paid is default
99
- @order.flow_data['delivered_duty'] || ::Io::Flow::V0::Models::DeliveredDuty.paid.value
100
- end
37
+ private
101
38
 
102
39
  # builds object that can be sent to api.flow.io to sync order data
103
40
  def build_flow_request
104
- @order.line_items.each { |line_item| add_item(line_item) }
41
+ @opts = { experience: @experience, expand: ['experience'] }
42
+ @body = { items: @order.line_items.map { |line_item| add_item(line_item) } }
105
43
 
106
- @opts = {}
107
- @opts[:experience] = @experience
108
- @opts[:expand] = ['experience']
44
+ try_to_add_customer
109
45
 
110
- @body = { items: @items, number: @order.number }
46
+ return unless (flow_data = @order.flow_data['order'])
111
47
 
112
- add_customer if @customer
48
+ @body[:selections] = flow_data['selections'].presence
49
+ @body[:delivered_duty] = flow_data['delivered_duty'].presence
50
+ @body[:attributes] = flow_data['attributes'].presence
113
51
 
114
- if (flow_data = @order.flow_data['order'])
115
- @body[:selections] = flow_data['selections'].presence
116
- @body[:delivered_duty] = flow_data['delivered_duty'].presence
117
- @body[:attributes] = flow_data['attributes'].presence
52
+ # discount on full order is applied
53
+ @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency } if @order.adjustment_total != 0
54
+ end
118
55
 
119
- if @order.adjustment_total != 0
120
- # discount on full order is applied
121
- @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency }
122
- end
123
- end
56
+ def refresh_checkout_token
57
+ root_url = Rails.application.routes.url_helpers.root_url
58
+ order_number = @order.number
59
+ confirmation_url = "#{root_url}flow/order-completed?order=#{order_number}&t=#{@order.guest_token}"
60
+ @order.flow_io_attribute_add('flow_return_url', confirmation_url)
61
+ @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
124
62
 
125
- # calculate digest body and cache it
126
- @digest = Digest::SHA1.hexdigest(@opts.to_json + @body.to_json)
63
+ FlowcommerceSpree.client.checkout_tokens.post_checkout_and_tokens_by_organization(
64
+ FlowcommerceSpree::ORGANIZATION, discriminator: 'checkout_token_reference_form',
65
+ order_number: order_number,
66
+ session_id: @flow_session_id,
67
+ urls: { continue_shopping: root_url,
68
+ confirmation: confirmation_url,
69
+ invalid_checkout: root_url }
70
+ )&.id
127
71
  end
128
72
 
129
- private
130
-
131
73
  # if customer is defined, add customer info
132
74
  # it is possible to have order in Spree without customer info (new guest session)
133
- def add_customer
134
- return unless @customer
135
-
136
- address = @customer.ship_address
137
- # address = nil
138
- if address
139
- @body[:customer] = { name: { first: address.firstname,
140
- last: address.lastname },
141
- email: @customer.email,
142
- number: @customer.flow_number,
143
- phone: address.phone }
144
-
145
- streets = []
146
- streets.push address.address1 unless address.address1.blank?
147
- streets.push address.address2 unless address.address2.blank?
148
-
149
- @body[:destination] = { streets: streets,
150
- city: address.city,
151
- province: address.state_name,
152
- postal: address.zipcode,
153
- country: (address.country.iso3 || 'USA'),
154
- contact: @body[:customer] }
155
-
156
- @body[:destination].delete_if { |_k, v| v.nil? }
157
- end
75
+ def try_to_add_customer
76
+ return unless (customer = @order.user)
158
77
 
159
- @body
160
- end
78
+ address = nil
79
+ customer_ship_address = customer.ship_address
80
+ address = customer_ship_address if customer_ship_address&.country&.iso3 == @order.zone.flow_io_experience_country
161
81
 
162
- def sync_body!
163
- build_flow_request if @body.blank?
82
+ customer_profile = customer.user_profile
83
+ unless address
84
+ user_profile_address = customer_profile&.address
85
+ address = user_profile_address if user_profile_address&.country&.iso3 == @order.zone.flow_io_experience_country
86
+ end
164
87
 
165
- @use_get = false
88
+ @body[:customer] = { name: { first: address&.firstname || customer_profile&.first_name,
89
+ last: address&.lastname || customer_profile&.last_name },
90
+ email: customer.email,
91
+ number: customer.flow_number,
92
+ phone: address&.phone }
166
93
 
167
- # use get if order is completed and closed
168
- @use_get = true if @order.state == 'complete'
94
+ add_customer_address(address) if address
95
+ end
169
96
 
170
- # use get if local digest hash check said there is no change
171
- @use_get ||= true if @order.flow_data['digest'] == @digest
97
+ def add_customer_address(address)
98
+ streets = []
99
+ streets.push address.address1 if address.address1.present?
100
+ streets.push address.address2 if address.address2.present?
172
101
 
173
- # do not use get if there is no local order cache
174
- @use_get = false unless @order.flow_data['order']
102
+ @body[:destination] = { streets: streets,
103
+ city: address.city,
104
+ province: address.state_name,
105
+ postal: address.zipcode,
106
+ country: (address.country&.iso3 || ''),
107
+ contact: @body[:customer] }
175
108
 
176
- if @use_get
177
- @response ||= FlowcommerceSpree::Api.run :get, "/:organization/orders/#{@body[:number]}", expand: 'experience'
178
- else
179
- @response = @client.orders.put_by_number(FlowcommerceSpree::ORGANIZATION, @order.number,
180
- Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
181
- end
109
+ @body[:destination].delete_if { |_k, v| v.nil? }
182
110
  end
183
111
 
184
- def check_state!
185
- # authorize if not authorized
186
- # if !@order.flow_order_authorized?
187
-
188
- # authorize payment on complete, unless authorized
189
- if @order.state == 'complete' && !@order.flow_order_authorized?
190
- simple_gateway = Flow::SimpleGateway.new(@order)
191
- simple_gateway.cc_authorization
192
- end
112
+ def sync_body!
113
+ build_flow_request
193
114
 
194
- @order.flow_finalize! if @order.flow_order_authorized? && @order.state != 'complete'
115
+ @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
116
+ Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
195
117
  end
196
118
 
197
119
  def add_item(line_item)
@@ -199,33 +121,19 @@ module FlowcommerceSpree
199
121
  price_root = variant.flow_data&.dig('exp', @experience, 'prices')&.[](0) || {}
200
122
 
201
123
  # create flow order line item
202
- item = { center: FLOW_CENTER,
203
- number: variant.sku,
204
- quantity: line_item.quantity,
205
- price: { amount: price_root['amount'] || variant.cost_price,
206
- currency: price_root['currency'] || variant.cost_currency } }
207
-
208
- @items.push item
124
+ { center: FLOW_CENTER,
125
+ number: variant.sku,
126
+ quantity: line_item.quantity,
127
+ price: { amount: price_root['amount'] || variant.cost_price,
128
+ currency: price_root['currency'] || variant.cost_currency } }
209
129
  end
210
130
 
211
- # set cache for total order ammount
212
- # written in flow_data field inside spree_orders table
213
- def write_response_in_cache
214
- if !@response || error?
215
- @order.flow_data.delete('digest')
216
- @order.flow_data.delete('order')
217
- else
218
- response_total = @response.dig('total', 'label')
219
- cache_total = @order.flow_data.dig('order', 'total', 'label')
220
-
221
- # return if total is not changed, no products removed or added
222
- return if @use_get && response_total == cache_total
223
-
224
- # update local order
225
- @order.flow_data.merge!('digest' => @digest, 'order' => @response.to_hash)
226
- end
131
+ def write_response_to_order
132
+ return @order.flow_data.delete('order') if !@response || error?
227
133
 
228
- @order.update_column(:meta, @order.meta.to_json)
134
+ # update local order
135
+ @order.total = @response[:total]&.[](:amount)
136
+ @order.flow_data.merge!('order' => @response)
229
137
  end
230
138
  end
231
139
  end