flowcommerce_spree 0.0.3 → 0.0.4

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/current_zone_loader_decorator.rb +5 -6
  3. data/app/controllers/flowcommerce_spree/inventory_controller.rb +23 -0
  4. data/app/controllers/flowcommerce_spree/orders_controller.rb +18 -0
  5. data/app/controllers/users/sessions_controller_decorator.rb +19 -2
  6. data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +0 -16
  7. data/app/models/spree/address_decorator.rb +1 -1
  8. data/app/models/spree/calculator/flow_io.rb +1 -1
  9. data/app/models/spree/flow_io_credit_card_decorator.rb +21 -0
  10. data/app/models/spree/{order_decorator.rb → flow_io_order_decorator.rb} +31 -65
  11. data/app/models/spree/gateway/flow_io.rb +61 -24
  12. data/app/models/spree/{credit_card_decorator.rb → payment_capture_event_decorator.rb} +1 -1
  13. data/app/serializers/api/v2/order_serializer_decorator.rb +20 -0
  14. data/app/services/flowcommerce_spree/import_experience_items.rb +1 -1
  15. data/app/services/flowcommerce_spree/order_sync.rb +26 -155
  16. data/app/services/flowcommerce_spree/order_updater.rb +76 -0
  17. data/app/views/spree/admin/payments/source_views/_flow_io_gateway.html.erb +21 -0
  18. data/config/routes.rb +2 -0
  19. data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +6 -4
  20. data/lib/flow/simple_gateway.rb +0 -36
  21. data/lib/flowcommerce_spree.rb +3 -1
  22. data/lib/flowcommerce_spree/engine.rb +1 -1
  23. data/lib/flowcommerce_spree/logging_http_client.rb +29 -13
  24. data/lib/flowcommerce_spree/session.rb +0 -18
  25. data/lib/flowcommerce_spree/version.rb +1 -1
  26. data/lib/flowcommerce_spree/webhook_service.rb +74 -104
  27. metadata +10 -19
  28. data/app/models/spree/line_item_decorator.rb +0 -15
@@ -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
@@ -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
 
@@ -16,41 +16,30 @@ module FlowcommerceSpree
16
16
  # flow_order.synchronize! # sends order to flow
17
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
20
19
 
21
- attr_reader :digest, :order, :response
20
+ attr_reader :order, :response
22
21
 
23
22
  delegate :url_helpers, to: 'Rails.application.routes'
24
23
 
25
- class << self
26
- def clear_cache(order)
27
- return unless order.flow_data['order']
28
-
29
- order.flow_data.delete('order')
30
- order.update_column :meta, order.meta.to_json
31
- end
32
- end
33
-
34
- def initialize(order:)
24
+ def initialize(order:, flow_session_id:)
35
25
  raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
36
26
 
37
27
  @experience = order.flow_io_experience_key
28
+ @flow_session_id = flow_session_id
38
29
  @order = order
39
- @client = FlowcommerceSpree.client(session_id: fetch_session_id)
30
+ @client = FlowcommerceSpree.client(session_id: flow_session_id)
40
31
  end
41
32
 
42
33
  # helper method to send complete order from Spree to flow.io
43
34
  def synchronize!
35
+ return unless @order.zone&.flow_io_active_experience? && @order.state == 'cart' && @order.line_items.size > 0
36
+
44
37
  sync_body!
45
- check_state!
46
38
  write_response_in_cache
47
39
 
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)
53
- @response
40
+ @order.update_columns(total: @order.total, meta: @order.meta.to_json)
41
+ refresh_checkout_token
42
+ @checkout_token
54
43
  end
55
44
 
56
45
  def error
@@ -65,46 +54,6 @@ module FlowcommerceSpree
65
54
  @response&.[]('code') && @response&.[]('messages') ? true : false
66
55
  end
67
56
 
68
- def delivery
69
- deliveries.select { |el| el[:active] }.first
70
- end
71
-
72
- # delivery methods are defined in flow console
73
- def deliveries
74
- # if we have error with an order, but still using this method
75
- return [] unless @order.flow_order
76
-
77
- @order.flow_data ||= {}
78
-
79
- delivery_list = @order.flow_order['deliveries'][0]['options'].map do |opts|
80
- name = opts['tier']['name']
81
-
82
- # add original Flow ID
83
- # name += ' (%s)' % opts['tier']['strategy'] if opts['tier']['strategy']
84
-
85
- selection_id = opts['id']
86
-
87
- { id: selection_id,
88
- price: { label: opts['price']['label'] },
89
- active: @order.flow_order['selections'].include?(selection_id),
90
- name: name }
91
- end.to_a
92
-
93
- # make first one active unless we have active element
94
- delivery_list.first[:active] = true unless delivery_list.select { |el| el[:active] }.first
95
-
96
- delivery_list
97
- end
98
-
99
- def total_price
100
- @order.flow_total
101
- end
102
-
103
- def delivered_duty
104
- # paid is default
105
- @order.flow_data['delivered_duty'] || ::Io::Flow::V0::Models::DeliveredDuty.paid.value
106
- end
107
-
108
57
  # builds object that can be sent to api.flow.io to sync order data
109
58
  def build_flow_request
110
59
  @opts = { experience: @experience, expand: ['experience'] }
@@ -112,94 +61,31 @@ module FlowcommerceSpree
112
61
 
113
62
  try_to_add_customer
114
63
 
115
- if (flow_data = @order.flow_data['order'])
116
- @body[:selections] = flow_data['selections'].presence
117
- @body[:delivered_duty] = flow_data['delivered_duty'].presence
118
- @body[:attributes] = flow_data['attributes'].presence
64
+ return unless (flow_data = @order.flow_data['order'])
119
65
 
120
- if @order.adjustment_total != 0
121
- # discount on full order is applied
122
- @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency }
123
- end
124
- end
66
+ @body[:selections] = flow_data['selections'].presence
67
+ @body[:delivered_duty] = flow_data['delivered_duty'].presence
68
+ @body[:attributes] = flow_data['attributes'].presence
125
69
 
126
- # calculate digest body and cache it
127
- @digest = Digest::SHA1.hexdigest(@opts.to_json + @body.to_json)
70
+ # discount on full order is applied
71
+ @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency } if @order.adjustment_total != 0
128
72
  end
129
73
 
130
74
  private
131
75
 
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
76
  def refresh_checkout_token
190
77
  root_url = url_helpers.root_url
191
78
  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(
79
+ confirmation_url = "#{root_url}flow/order-completed?order=#{order_number}&t=#{@order.guest_token}"
80
+ @checkout_token = FlowcommerceSpree.client.checkout_tokens.post_checkout_and_tokens_by_organization(
194
81
  FlowcommerceSpree::ORGANIZATION,
195
82
  discriminator: 'checkout_token_reference_form',
196
83
  order_number: order_number,
197
- session_id: @order.flow_data['session_id'],
84
+ session_id: @flow_session_id,
198
85
  urls: { continue_shopping: root_url,
199
86
  confirmation: confirmation_url,
200
87
  invalid_checkout: root_url }
201
- )
202
- @order.add_flow_checkout_token(checkout_token.id)
88
+ )&.id
203
89
 
204
90
  @order.flow_io_attribute_add('flow_return_url', confirmation_url)
205
91
  @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
@@ -244,16 +130,13 @@ module FlowcommerceSpree
244
130
  end
245
131
 
246
132
  def sync_body!
247
- build_flow_request if @body.blank?
133
+ build_flow_request
248
134
 
249
135
  @use_get = false
250
136
 
251
137
  # use get if order is completed and closed
252
138
  @use_get = true if @order.flow_data.dig('order', 'submitted_at').present? || @order.state == 'complete'
253
139
 
254
- # use get if local digest hash check said there is no change
255
- @use_get ||= true if @order.flow_data['digest'] == @digest
256
-
257
140
  # do not use get if there is no local order cache
258
141
  @use_get = false unless @order.flow_data['order']
259
142
 
@@ -265,19 +148,6 @@ module FlowcommerceSpree
265
148
  end
266
149
  end
267
150
 
268
- def check_state!
269
- # authorize if not authorized
270
- # if !@order.flow_order_authorized?
271
-
272
- # authorize payment on complete, unless authorized
273
- if @order.state == 'complete' && !@order.flow_order_authorized?
274
- simple_gateway = Flow::SimpleGateway.new(@order)
275
- simple_gateway.cc_authorization
276
- end
277
-
278
- @order.flow_finalize! if @order.flow_order_authorized? && @order.state != 'complete'
279
- end
280
-
281
151
  def add_item(line_item)
282
152
  variant = line_item.variant
283
153
  price_root = variant.flow_data&.dig('exp', @experience, 'prices')&.[](0) || {}
@@ -294,17 +164,18 @@ module FlowcommerceSpree
294
164
  # written in flow_data field inside spree_orders table
295
165
  def write_response_in_cache
296
166
  if !@response || error?
297
- @order.flow_data.delete('digest')
298
167
  @order.flow_data.delete('order')
299
168
  else
300
- response_total = @response.dig('total', 'label')
301
- cache_total = @order.flow_data.dig('order', 'total', 'label')
169
+ response_total = @response[:total]
170
+ response_total_label = response_total&.[](:label)
171
+ cache_total = @order.flow_data.dig('order', 'total', 'label')
302
172
 
303
173
  # return if total is not changed, no products removed or added
304
- return if @use_get && response_total == cache_total
174
+ return if @use_get && response_total_label == cache_total
305
175
 
306
176
  # update local order
307
- @order.flow_data.merge!('digest' => @digest, 'order' => @response)
177
+ @order.total = response_total&.[](:amount)
178
+ @order.flow_data.merge!('order' => @response)
308
179
  end
309
180
  end
310
181
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class OrderUpdater
5
+ def initialize(order:)
6
+ raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
7
+
8
+ @experience = order.flow_io_experience_key
9
+ @order = order
10
+ @client = FlowcommerceSpree.client
11
+ end
12
+
13
+ def upsert_data(flow_io_order = nil)
14
+ return if @order.state == 'complete'
15
+
16
+ flow_io_order ||= @client.orders.get_by_number(FlowcommerceSpree::ORGANIZATION, @order.number).to_hash
17
+
18
+ @order.flow_data['order'] = flow_io_order
19
+ return if @order.flow_data.dig('order', 'submitted_at').blank?
20
+
21
+ attrs_to_update = { meta: @order.meta.to_json, email: @order.flow_customer_email, payment_state: 'pending' }
22
+ attrs_to_update.merge!(@order.prepare_flow_addresses)
23
+ @order.update_columns(attrs_to_update)
24
+ @order.state = 'delivery'
25
+ @order.save!
26
+ @order.create_proposed_shipments
27
+ @order.shipment.update_amounts
28
+ @order.line_items.each(&:store_ets)
29
+ @order.charge_taxes
30
+
31
+ @order.state = 'payment'
32
+ @order.save!
33
+ end
34
+
35
+ def finalize_order
36
+ @order.reload
37
+ @order.finalize!
38
+ @order.update_totals
39
+ @order.save
40
+ @order.after_completed_order
41
+ end
42
+
43
+ def complete_checkout
44
+ upsert_data
45
+ map_payments_to_spree
46
+ finalize_order if @order.state == 'complete'
47
+ end
48
+
49
+ def map_payments_to_spree
50
+ @order.flow_io_payments&.each do |p|
51
+ payment =
52
+ @order.payments.find_or_initialize_by(response_code: p['reference'], payment_method_id: payment_method_id)
53
+ next unless payment.new_record?
54
+
55
+ payment.amount = p.dig('total', 'amount')
56
+ payment.pend
57
+
58
+ # For now this additional update is overwriting the generated identifier with flow.io payment identifier.
59
+ # TODO: Check and possibly refactor in Spree 3.0, where the `before_create :set_unique_identifier`
60
+ # has been removed.
61
+ payment.update_column(:identifier, p['id'])
62
+ end
63
+
64
+ return if @order.payments.sum(:amount) < @order.amount || @order.state == 'complete'
65
+
66
+ @order.state = 'confirm'
67
+ @order.save!
68
+ @order.state = 'complete'
69
+ @order.save!
70
+ end
71
+
72
+ def payment_method_id
73
+ @payment_method_id ||= Spree::PaymentMethod.find_by(active: true, type: 'Spree::Gateway::FlowIo').id
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,21 @@
1
+ <fieldset data-hook="credit_card">
2
+ <legend align="center"><%= Spree.t(:credit_card) %></legend>
3
+
4
+ <div class="row">
5
+ <div class="alpha six columns">
6
+ <dl>
7
+ <dt><%= Spree.t(:name_on_card) %>:</dt>
8
+ <dd><%= payment.source&.name %></dd>
9
+
10
+ <dt><%= Spree.t(:card_type) %>:</dt>
11
+ <dd><%= payment.source&.cc_type %></dd>
12
+
13
+ <dt><%= Spree.t(:card_number) %>:</dt>
14
+ <dd><%= payment.source&.display_number %></dd>
15
+
16
+ <dt><%= Spree.t(:expiration) %>:</dt>
17
+ <dd><%= payment.source&.month %>/<%= payment.source&.year %></dd>
18
+ </dl>
19
+ </div>
20
+ </div>
21
+ </fieldset>
data/config/routes.rb CHANGED
@@ -2,4 +2,6 @@
2
2
 
3
3
  FlowcommerceSpree::Engine.routes.draw do
4
4
  post '/event-target', to: 'webhooks#handle_flow_web_hook_event'
5
+ get '/order-completed', to: 'orders#order_completed'
6
+ post '/online-stock-availability', to: 'inventory#online_stock_availability'
5
7
  end
@@ -5,13 +5,15 @@ class AddMetaToSpreeTables < ActiveRecord::Migration
5
5
  add_column :spree_orders, :meta, :jsonb, default: '{}' unless column_exists?(:spree_orders, :meta)
6
6
  add_column :spree_promotions, :meta, :jsonb, default: '{}' unless column_exists?(:spree_promotions, :meta)
7
7
  add_column :spree_credit_cards, :meta, :jsonb, default: '{}' unless column_exists?(:spree_credit_cards, :meta)
8
+ add_column :spree_payment_capture_events, :meta, :jsonb, default: '{}' unless column_exists?(:spree_payment_capture_events, :meta)
8
9
  end
9
10
 
10
11
  def down
11
- remove_column :spree_products, :meta if column_exists?(:spree_products, :meta)
12
- remove_column :spree_variants, :meta if column_exists?(:spree_variants, :meta)
13
- remove_column :spree_orders, :meta if column_exists?(:spree_orders, :meta)
14
- remove_column :spree_promotions, :meta if column_exists?(:spree_promotions, :meta)
12
+ remove_column :spree_payment_capture_events, :meta if column_exists?(:spree_payment_capture_events, :meta)
15
13
  remove_column :spree_credit_cards, :meta if column_exists?(:spree_credit_cards, :meta)
14
+ remove_column :spree_promotions, :meta if column_exists?(:spree_promotions, :meta)
15
+ remove_column :spree_orders, :meta if column_exists?(:spree_orders, :meta)
16
+ remove_column :spree_variants, :meta if column_exists?(:spree_variants, :meta)
17
+ remove_column :spree_products, :meta if column_exists?(:spree_products, :meta)
16
18
  end
17
19
  end
@@ -34,42 +34,6 @@ module Flow
34
34
  error_response(e)
35
35
  end
36
36
 
37
- # capture authorised funds
38
- def cc_capture
39
- # GET /:organization/authorizations, order_number: abc
40
- data = @order.flow_data['authorization']
41
-
42
- raise ArgumentError, 'No Authorization data, please authorize first' unless data
43
-
44
- capture_form = ::Io::Flow::V0::Models::CaptureForm.new(data)
45
- response = FlowcommerceSpree.client.captures.post(FlowcommerceSpree::ORGANIZATION, capture_form)
46
-
47
- return ActiveMerchant::Billing::Response.new false, 'error', response: response unless response.id
48
-
49
- @order.update_column :flow_data, @order.flow_data.merge('capture': response.to_hash)
50
- @order.flow_finalize!
51
-
52
- ActiveMerchant::Billing::Response.new true, 'success', response: response
53
- rescue StandardError => e
54
- error_response(e)
55
- end
56
-
57
- def cc_refund
58
- raise ArgumentError, 'capture info is not available' unless @order.flow_data['capture']
59
-
60
- # we allways have capture ID, so we use it
61
- refund_data = { capture_id: @order.flow_data['capture']['id'] }
62
- refund_form = ::Io::Flow::V0::Models::RefundForm.new(refund_data)
63
- response = FlowcommerceSpree.client.refunds.post(FlowcommerceSpree::ORGANIZATION, refund_form)
64
-
65
- return ActiveMerchant::Billing::Response.new false, 'error', response: response unless response.id
66
-
67
- @order.update_column :flow_data, @order.flow_data.merge('refund': response.to_hash)
68
- ActiveMerchant::Billing::Response.new true, 'success', response: response
69
- rescue StandardError => e
70
- error_response(e)
71
- end
72
-
73
37
  private
74
38
 
75
39
  # if order is not in flow, we use local Spree settings