flowcommerce_spree 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -5,20 +5,23 @@ module FlowcommerceSpree
5
5
  # for easy integration we are currently passing:
6
6
  # - flow experience
7
7
  # - spree order
8
- # - current customer, present as @current_spree_user controller instance variable
8
+ # - current customer, if present as @order.user
9
9
  #
10
10
  # example:
11
11
  # flow_order = FlowcommerceSpree::OrderSync.new # init flow-order object
12
12
  # order: Spree::Order.last,
13
13
  # experience: @flow_session.experience
14
- # customer: Spree::User.last
14
+ # customer: @order.user
15
15
  # flow_order.build_flow_request # builds json body to be posted to flow.io api
16
16
  # flow_order.synchronize! # sends order to flow
17
- class OrderSync
17
+ class OrderSync # rubocop:disable Metrics/ClassLength
18
18
  FLOW_CENTER = 'default'
19
+ SESSION_EXPIRATION_THRESHOLD = 10 # Refresh session if less than 10 seconds to session expiration remains
19
20
 
20
21
  attr_reader :digest, :order, :response
21
22
 
23
+ delegate :url_helpers, to: 'Rails.application.routes'
24
+
22
25
  class << self
23
26
  def clear_cache(order)
24
27
  return unless order.flow_data['order']
@@ -31,11 +34,9 @@ module FlowcommerceSpree
31
34
  def initialize(order:)
32
35
  raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
33
36
 
34
- @client = FlowcommerceSpree.client(session_id: order.flow_data['session_id'])
35
37
  @experience = order.flow_io_experience_key
36
- @order = order
37
- @customer = order.user
38
- @items = []
38
+ @order = order
39
+ @client = FlowcommerceSpree.client(session_id: fetch_session_id)
39
40
  end
40
41
 
41
42
  # helper method to send complete order from Spree to flow.io
@@ -43,6 +44,12 @@ module FlowcommerceSpree
43
44
  sync_body!
44
45
  check_state!
45
46
  write_response_in_cache
47
+
48
+ # This is for 1st order syncing, when no checkout_token has been fetched yet. In all the subsequent syncs,
49
+ # the checkout_token is fetched in the `fetch_session_id` method, calling the refresh_checkout_token method when
50
+ # necessary.
51
+ refresh_checkout_token if @order.flow_io_checkout_token.blank?
52
+ @order.update_column(:meta, @order.meta.to_json)
46
53
  @response
47
54
  end
48
55
 
@@ -64,13 +71,12 @@ module FlowcommerceSpree
64
71
 
65
72
  # delivery methods are defined in flow console
66
73
  def deliveries
67
- # if we have erorr with an order, but still using this method
74
+ # if we have error with an order, but still using this method
68
75
  return [] unless @order.flow_order
69
76
 
70
77
  @order.flow_data ||= {}
71
78
 
72
- delivery_list = @order.flow_order['deliveries'][0]['options']
73
- delivery_list = delivery_list.map do |opts|
79
+ delivery_list = @order.flow_order['deliveries'][0]['options'].map do |opts|
74
80
  name = opts['tier']['name']
75
81
 
76
82
  # add original Flow ID
@@ -101,15 +107,10 @@ module FlowcommerceSpree
101
107
 
102
108
  # builds object that can be sent to api.flow.io to sync order data
103
109
  def build_flow_request
104
- @order.line_items.each { |line_item| add_item(line_item) }
105
-
106
- @opts = {}
107
- @opts[:experience] = @experience
108
- @opts[:expand] = ['experience']
110
+ @opts = { experience: @experience, expand: ['experience'] }
111
+ @body = { items: @order.line_items.map { |line_item| add_item(line_item) } }
109
112
 
110
- @body = { items: @items, number: @order.number }
111
-
112
- add_customer if @customer
113
+ try_to_add_customer
113
114
 
114
115
  if (flow_data = @order.flow_data['order'])
115
116
  @body[:selections] = flow_data['selections'].presence
@@ -128,35 +129,118 @@ module FlowcommerceSpree
128
129
 
129
130
  private
130
131
 
132
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
133
+ def fetch_session_id
134
+ session = RequestStore.store[:session]
135
+ current_session_id = session&.[]('_f60_session')
136
+ session_expire_at = session&.[]('_f60_expires_at')&.to_datetime
137
+ session_expired = flow_io_session_expired?(session_expire_at.to_i)
138
+ order_flow_session_id = @order.flow_data['session_id']
139
+ order_session_expire_at = @order.flow_io_session_expires_at
140
+ order_session_expired = flow_io_session_expired?(order_session_expire_at.to_i)
141
+
142
+ if order_flow_session_id == current_session_id && session_expire_at == order_session_expire_at &&
143
+ @order.flow_io_checkout_token.present? && session_expired == false
144
+ return current_session_id
145
+ elsif current_session_id && session_expire_at && session_expired == false
146
+ # If request flow_session is not expired, don't refresh the flow_session (i.e., don't mark the refresh_session
147
+ # lvar as true), just store the flow_session data into the order, if it is new, and refresh the checkout_token
148
+ refresh_session = nil
149
+ elsif order_flow_session_id && order_session_expire_at && order_session_expired == false && session_expired.nil?
150
+ refresh_checkout_token if @order.flow_io_order_id && @order.flow_io_checkout_token.blank?
151
+ return order_flow_session_id
152
+ else
153
+ refresh_session = true
154
+ end
155
+
156
+ if refresh_session
157
+ flow_io_session = Session.new(
158
+ ip: '127.0.0.1',
159
+ visitor: "session-#{Digest::SHA1.hexdigest(@order.guest_token)}",
160
+ experience: @experience
161
+ )
162
+ flow_io_session.create
163
+ current_session_id = flow_io_session.id
164
+ session_expire_at = flow_io_session.expires_at.to_s
165
+ end
166
+
167
+ @order.flow_data['session_id'] = current_session_id
168
+ @order.flow_data['session_expires_at'] = session_expire_at
169
+
170
+ if session.respond_to?(:[])
171
+ session['_f60_session'] = current_session_id
172
+ session['_f60_expires_at'] = session_expire_at
173
+ end
174
+
175
+ # On the 1st OrderSync at this moment the order is not yet created at flow.io, so we couldn't yet retrieve the
176
+ # checkout_token. This is done after the order will be synced, in the `synchronize!` method.
177
+ refresh_checkout_token if @order.flow_io_order_id
178
+
179
+ current_session_id
180
+ end
181
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
182
+
183
+ def flow_io_session_expired?(expiration_time)
184
+ return nil if expiration_time == 0
185
+
186
+ expiration_time - Time.zone.now.utc.to_i < SESSION_EXPIRATION_THRESHOLD
187
+ end
188
+
189
+ def refresh_checkout_token
190
+ root_url = url_helpers.root_url
191
+ order_number = @order.number
192
+ confirmation_url = "#{root_url}thankyou?order=#{order_number}&t=#{@order.guest_token}"
193
+ checkout_token = FlowcommerceSpree.client.checkout_tokens.post_checkout_and_tokens_by_organization(
194
+ FlowcommerceSpree::ORGANIZATION,
195
+ discriminator: 'checkout_token_reference_form',
196
+ order_number: order_number,
197
+ session_id: @order.flow_data['session_id'],
198
+ urls: { continue_shopping: root_url,
199
+ confirmation: confirmation_url,
200
+ invalid_checkout: root_url }
201
+ )
202
+ @order.add_flow_checkout_token(checkout_token.id)
203
+
204
+ @order.flow_io_attribute_add('flow_return_url', confirmation_url)
205
+ @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
206
+ end
207
+
131
208
  # if customer is defined, add customer info
132
209
  # 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? }
210
+ def try_to_add_customer
211
+ return unless (customer = @order.user)
212
+
213
+ address = nil
214
+ customer_ship_address = customer.ship_address
215
+ address = customer_ship_address if customer_ship_address&.country&.iso3 == @order.zone.flow_io_experience_country
216
+
217
+ unless address
218
+ user_profile_address = customer.user_profile&.address
219
+ address = user_profile_address if user_profile_address&.country&.iso3 == @order.zone.flow_io_experience_country
157
220
  end
158
221
 
159
- @body
222
+ @body[:customer] = { name: { first: address&.firstname,
223
+ last: address&.lastname },
224
+ email: customer.email,
225
+ number: customer.flow_number,
226
+ phone: address&.phone }
227
+
228
+ add_customer_address(address) if address
229
+ end
230
+
231
+ def add_customer_address(address)
232
+ streets = []
233
+ streets.push address.address1 if address&.address1.present?
234
+ streets.push address.address2 if address&.address2.present?
235
+
236
+ @body[:destination] = { streets: streets,
237
+ city: address&.city,
238
+ province: address&.state_name,
239
+ postal: address&.zipcode,
240
+ country: (address&.country&.iso3 || ''),
241
+ contact: @body[:customer] }
242
+
243
+ @body[:destination].delete_if { |_k, v| v.nil? }
160
244
  end
161
245
 
162
246
  def sync_body!
@@ -165,7 +249,7 @@ module FlowcommerceSpree
165
249
  @use_get = false
166
250
 
167
251
  # use get if order is completed and closed
168
- @use_get = true if @order.state == 'complete'
252
+ @use_get = true if @order.flow_data.dig('order', 'submitted_at').present? || @order.state == 'complete'
169
253
 
170
254
  # use get if local digest hash check said there is no change
171
255
  @use_get ||= true if @order.flow_data['digest'] == @digest
@@ -174,9 +258,9 @@ module FlowcommerceSpree
174
258
  @use_get = false unless @order.flow_data['order']
175
259
 
176
260
  if @use_get
177
- @response ||= FlowcommerceSpree::Api.run :get, "/:organization/orders/#{@body[:number]}", expand: 'experience'
261
+ @response ||= @client.orders.get_by_number(ORGANIZATION, @order.number).to_hash
178
262
  else
179
- @response = @client.orders.put_by_number(FlowcommerceSpree::ORGANIZATION, @order.number,
263
+ @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
180
264
  Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
181
265
  end
182
266
  end
@@ -199,16 +283,14 @@ module FlowcommerceSpree
199
283
  price_root = variant.flow_data&.dig('exp', @experience, 'prices')&.[](0) || {}
200
284
 
201
285
  # 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
286
+ { center: FLOW_CENTER,
287
+ number: variant.sku,
288
+ quantity: line_item.quantity,
289
+ price: { amount: price_root['amount'] || variant.cost_price,
290
+ currency: price_root['currency'] || variant.cost_currency } }
209
291
  end
210
292
 
211
- # set cache for total order ammount
293
+ # set cache for total order amount
212
294
  # written in flow_data field inside spree_orders table
213
295
  def write_response_in_cache
214
296
  if !@response || error?
@@ -222,10 +304,8 @@ module FlowcommerceSpree
222
304
  return if @use_get && response_total == cache_total
223
305
 
224
306
  # update local order
225
- @order.flow_data.merge!('digest' => @digest, 'order' => @response.to_hash)
307
+ @order.flow_data.merge!('digest' => @digest, 'order' => @response)
226
308
  end
227
-
228
- @order.update_column(:meta, @order.meta.to_json)
229
309
  end
230
310
  end
231
311
  end
@@ -0,0 +1,51 @@
1
+ AddModelVirtualAttributeCheck: { }
2
+ AlwaysAddDbIndexCheck: { }
3
+ #CheckSaveReturnValueCheck: { }
4
+ #CheckDestroyReturnValueCheck: { }
5
+ DefaultScopeIsEvilCheck: { }
6
+ DryBundlerInCapistranoCheck: { }
7
+ #HashSyntaxCheck: { }
8
+ IsolateSeedDataCheck: { }
9
+ KeepFindersOnTheirOwnModelCheck: { }
10
+ LawOfDemeterCheck: { }
11
+ #LongLineCheck: { max_line_length: 80 }
12
+ MoveCodeIntoControllerCheck: { }
13
+ MoveCodeIntoHelperCheck: { array_count: 3 }
14
+ MoveCodeIntoModelCheck: { use_count: 2 }
15
+ MoveFinderToNamedScopeCheck: { }
16
+ MoveModelLogicIntoModelCheck: { use_count: 4 }
17
+ NeedlessDeepNestingCheck: { nested_count: 2 }
18
+ NotRescueExceptionCheck: { }
19
+ NotUseDefaultRouteCheck: { }
20
+ NotUseTimeAgoInWordsCheck: { }
21
+ OveruseRouteCustomizationsCheck: { customize_count: 3 }
22
+ ProtectMassAssignmentCheck: { }
23
+ RemoveEmptyHelpersCheck: { }
24
+ #RemoveTabCheck: { }
25
+ RemoveTrailingWhitespaceCheck: { }
26
+ RemoveUnusedMethodsInControllersCheck: { except_methods: [] }
27
+ RemoveUnusedMethodsInHelpersCheck: { except_methods: [] }
28
+ RemoveUnusedMethodsInModelsCheck: { except_methods:
29
+ [
30
+ 'Spree::Calculator::FlowIo#compute_shipment', # Used by Spree::Calculator
31
+ 'Spree::Calculator::FlowIo#compute_line_item', # Used by Spree::Calculator
32
+ 'Spree::Calculator::FlowIo#description', # Used by Spree
33
+ 'Spree::Shipping::FlowIo#compute_package', # Used by Spree
34
+ 'Spree::Shipping::FlowIo#default_charge', # Used by Spree
35
+ 'Spree::Shipping::FlowIo#threshold', # Used by Spree
36
+ 'Spree::Shipping::FlowIo#description', # Used by Spree
37
+ ] }
38
+ ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
39
+ ReplaceInstanceVariableWithLocalVariableCheck: { }
40
+ RestrictAutoGeneratedRoutesCheck: { }
41
+ SimplifyRenderInControllersCheck: { }
42
+ SimplifyRenderInViewsCheck: { }
43
+ #UseBeforeFilterCheck: { customize_count: 2 }
44
+ UseModelAssociationCheck: { }
45
+ UseMultipartAlternativeAsContentTypeOfEmailCheck: { }
46
+ #UseParenthesesInMethodDefCheck: { }
47
+ UseObserverCheck: { }
48
+ UseQueryAttributeCheck: { }
49
+ UseSayWithTimeInMigrationsCheck: { }
50
+ UseScopeAccessCheck: { }
51
+ UseTurboSprocketsRails3Check: { }
@@ -9,6 +9,7 @@ require 'flowcommerce_spree/logging_http_handler'
9
9
  require 'flowcommerce_spree/webhook_service'
10
10
  require 'flowcommerce_spree/session'
11
11
  require 'flow/simple_gateway'
12
+ require 'request_store'
12
13
 
13
14
  module FlowcommerceSpree
14
15
  def self.client(logger: FlowcommerceSpree.logger, **opts)
@@ -20,11 +21,20 @@ module FlowcommerceSpree
20
21
  end
21
22
 
22
23
  def self.logger
23
- logger = ActiveSupport::Logger.new(STDOUT, 3, 10_485_760)
24
+ logger = ActiveSupport::Logger.new(STDOUT)
25
+
26
+ logger_formatter = proc do |severity, datetime, _progname, msg|
27
+ "\n#{datetime}, #{severity}: #{msg}\n"
28
+ end
29
+
30
+ logger.formatter = logger_formatter
24
31
 
25
32
  # Broadcast the log into the file besides STDOUT, if `log` folder exists
26
33
  if Dir.exist?('log')
27
- logger.extend(ActiveSupport::Logger.broadcast(ActiveSupport::Logger.new('log/flowcommerce_spree.log')))
34
+ file_logger = ActiveSupport::Logger.new('log/flowcommerce_spree.log', 3, 10_485_760)
35
+ file_logger.formatter = logger_formatter
36
+
37
+ logger.extend(ActiveSupport::Logger.broadcast(file_logger))
28
38
  end
29
39
  logger
30
40
  end
@@ -6,14 +6,34 @@ module FlowcommerceSpree
6
6
  isolate_namespace FlowcommerceSpree
7
7
 
8
8
  config.before_initialize do
9
+ FlowcommerceSpree::ORGANIZATION = ENV.fetch('FLOW_ORGANIZATION', 'flow.io')
10
+ FlowcommerceSpree::BASE_COUNTRY = ENV.fetch('FLOW_BASE_COUNTRY', 'USA')
11
+ FlowcommerceSpree::API_KEY = ENV.fetch('FLOW_TOKEN', 'test_key')
12
+
9
13
  FlowcommerceSpree::Config = FlowcommerceSpree::Settings.new
10
14
  end
11
15
 
12
- config.after_initialize do
16
+ config.flowcommerce_spree = ActiveSupport::OrderedOptions.new
17
+
18
+ initializer 'flowcommerce_spree.configuration' do |app|
19
+ # If some Rake tasks will fail in development environment, the cause could be the autoloading.
20
+ # Uncommenting the following 3 lines will enable eager-loading for the flowcommerce_spree Rake tasks.
21
+ # if Rails.env.development?
22
+ # app.config.eager_load = Rake.application.top_level_tasks.any? { |t| t.start_with?('flowcommerce_spree') }
23
+ # end
24
+
25
+ app.config.flowcommerce_spree[:mounted_path] = ENV.fetch('FLOW_MOUNT_PATH', '/flow')
26
+
27
+ app.routes.append do
28
+ mount FlowcommerceSpree::Engine => app.config.flowcommerce_spree[:mounted_path]
29
+ end
30
+ end
31
+
32
+ config.after_initialize do |app|
13
33
  # init Flow payments as an option
14
- # app.config.spree.payment_methods << Spree::Gateway::Flow
34
+ app.config.spree.payment_methods << Spree::Gateway::FlowIo
15
35
 
16
- Flow::SimpleGateway.clear_zero_amount_payments = true
36
+ # Flow::SimpleGateway.clear_zero_amount_payments = true
17
37
  end
18
38
 
19
39
  def self.activate
@@ -23,5 +43,10 @@ module FlowcommerceSpree
23
43
  end
24
44
 
25
45
  config.to_prepare(&method(:activate).to_proc)
46
+
47
+ initializer 'spree.flowcommerce_spree.calculators', after: 'spree.register.calculators' do |_app|
48
+ Rails.application.config.spree.calculators.tax_rates << Spree::Calculator::FlowIo
49
+ Rails.application.config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::FlowIo
50
+ end
26
51
  end
27
52
  end