flowcommerce_spree 0.0.3 → 0.0.4

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