flowcommerce_spree 0.0.2 → 0.0.3

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.
@@ -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