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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51a668ee3cc70d0bc9bd8c7662a972f086e06dcdcf4c68df2177e94818dfb820
4
- data.tar.gz: aa9ff17e57cd19452736af11d51bd1905c4dd7e66bb461b5dff017fc5dea0b0d
3
+ metadata.gz: 3810faadd82b0d21c0bacaeaa6be61bfe314ddffe4f32a6805c05554c592a745
4
+ data.tar.gz: 6d042ed69b193082492ac992ea3d4a9bd962e493ae49026a43c297492013b4ea
5
5
  SHA512:
6
- metadata.gz: 024a29a6fc47f932e432f2477f9a1f3b520af7cc10b7a85eb35613328276d5390a682187151217c0527b20435431f78e8cc55255d95298620bfb5077c7ed5011
7
- data.tar.gz: b6d9485612e360ed50caabfab537baf9583feec384607b4a890c1ab1fd9fc48cd9ea3954562aac828f4a858682ad1515ae7f2b9bf05a8a501ff8abf96c28f972
6
+ metadata.gz: 6bd6d8e87e8f5d8c5d30d9f4366bc0e6d06dc95504d6f8cd3fc8916c752478123e80671ac5da4063a02c6051eb56d95f14ef97d782249a7fa9cb1df4e983829f
7
+ data.tar.gz: 260d3b14add605bc114bd561a39f7376a88850d4aea95a98339db5461a9845463a87da1cd845cf68a6b8732a88d7f445a8961fb4f5a0847c282130efeb247083
@@ -20,12 +20,11 @@ CurrentZoneLoader.module_eval do
20
20
  session['region'] = { name: current_zone_name, available_currencies: @current_zone.available_currencies,
21
21
  request_iso_code: request_iso_code }
22
22
 
23
- RequestStore.store[:session] = session
24
23
  Rails.logger.debug("Using product zone: #{current_zone_name}")
25
24
  @current_zone
26
25
  end
27
26
 
28
- def flow_zone # rubocop:disable Metrics/AbcSize
27
+ def flow_zone
29
28
  return unless Spree::Zones::Product.active
30
29
  .where("meta -> 'flow_data' ->> 'country' = ?",
31
30
  ISO3166::Country[request_iso_code]&.alpha3).exists?
@@ -36,13 +35,13 @@ CurrentZoneLoader.module_eval do
36
35
  Spree::Config[:debug_request_ip_address] || request.ip
37
36
  # Germany ip: 85.214.132.117, Sweden ip: 62.20.0.196, Moldova ip: 89.41.76.29
38
37
  end
38
+
39
+ # This will issue a session creation request to flow.io. The response will contain the Flow Experience key and
40
+ # the session_id
39
41
  flow_io_session = FlowcommerceSpree::Session.create(ip: request_ip, visitor: visitor_id_for_flow_io)
40
- # :create method will issue a request to flow.io. The experience, contained in the
41
- # response, will be available in the session object - flow_io_session.experience
42
42
 
43
43
  if (zone = Spree::Zones::Product.active.find_by(name: flow_io_session.experience&.key&.titleize))
44
- session['_f60_session'] = flow_io_session.id
45
- session['_f60_expires_at'] = flow_io_session.expires_at.to_s
44
+ session['flow_session_id'] = flow_io_session.id
46
45
  end
47
46
 
48
47
  zone
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class InventoryController < ActionController::Base
5
+ def online_stock_availability
6
+ items = params['items'] || []
7
+ response = items.inject([]) { |result, item| result << check_stock(item[:id], item[:qty].to_i) }
8
+ render json: { items: response }, status: :ok
9
+ end
10
+
11
+ private
12
+
13
+ def check_stock(flow_number, quantity)
14
+ variant = Spree::Variant.find_by(sku: flow_number)
15
+ return { id: flow_number, has_inventory: false } unless variant
16
+
17
+ { id: flow_number, has_inventory: variant.available_online?(quantity) }
18
+ rescue StandardError
19
+ Rails.logger.error "[!] FlowcommerceSpree::InventoryController#stock unexpected Error: #{$ERROR_INFO}"
20
+ { id: flow_number, has_inventory: false }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class OrdersController < ApplicationController
5
+ wrap_parameters false
6
+
7
+ skip_before_action :setup_tracking, only: :order_completed
8
+
9
+ # proxy enpoint between flow and thankyou page.
10
+ # /flow/order_completed endpoint
11
+ def order_completed
12
+ flow_updater = FlowcommerceSpree::OrderUpdater.new(order: current_order)
13
+ flow_updater.complete_checkout
14
+
15
+ redirect_to "/thankyou?order=#{params[:order]}&t=#{params[:t]}"
16
+ end
17
+ end
18
+ end
@@ -2,10 +2,27 @@
2
2
 
3
3
  module Users
4
4
  SessionsController.class_eval do
5
+ # This endpoint is for returning to the FrontEnd the dynamic url to an external checkout, a flow.io url.
6
+ def checkout_url
7
+ flow_session_id = request.headers['flow-session-id']
8
+ return render json: { error: :session_id_missing }, status: 422 if flow_session_id.blank?
9
+
10
+ checkout_token =
11
+ FlowcommerceSpree::OrderSync.new(order: current_order, flow_session_id: flow_session_id).synchronize!
12
+ return render json: { error: :checkout_token_missing }, status: 422 if checkout_token.blank?
13
+
14
+ render json: { checkout_url: "https://checkout.flow.io/tokens/#{checkout_token}" }, status: 200
15
+ end
16
+
5
17
  private
6
18
 
7
- def external_checkout?
8
- current_zone.flow_io_active_experience? ? 'true' : 'false'
19
+ def add_optional_attrs(session_current)
20
+ session_current['user'] = current_user_attrs if current_user&.spree_api_key?
21
+ session_current['region'] = zone_attrs
22
+
23
+ external_checkout = current_zone.flow_io_active_experience?
24
+ session_current['external_checkout'] = external_checkout
25
+ session_current['flow_session_id'] = session['flow_session_id'] if external_checkout
9
26
  end
10
27
  end
11
28
  end
@@ -17,22 +17,6 @@ module Spree
17
17
  @current_order.flow_io_experience_from_zone
18
18
  update_meta ||= true
19
19
  end
20
- order_flow_session_id = @current_order.flow_data['session_id']
21
- order_session_expired = @current_order.flow_data['session_expires_at']
22
- flow_io_session_id = session['_f60_session']
23
- flow_io_session_expires = session['_f60_expires_at']
24
- if flow_io_session_id.present?
25
- if order_flow_session_id != flow_io_session_id &&
26
- order_session_expired&.to_datetime.to_i < flow_io_session_expires&.to_datetime.to_i
27
- @current_order.flow_data['session_id'] = flow_io_session_id
28
- @current_order.flow_data['session_expires_at'] = flow_io_session_expires
29
- @current_order.flow_data['checkout_token'] = nil
30
- update_meta ||= true
31
- end
32
- elsif order_flow_session_id.present?
33
- session['_f60_session'] = order_flow_session_id
34
- session['_f60_expires_at'] = order_session_expired
35
- end
36
20
  end
37
21
 
38
22
  if @current_order.new_record?
@@ -11,7 +11,7 @@ module Spree
11
11
  address2: address_data['streets'][1],
12
12
  zipcode: address_data['postal'],
13
13
  city: address_data['city'],
14
- state_name: address_data['province'] || 'something',
14
+ state_name: address_data['province'],
15
15
  country: Spree::Country.find_by(iso3: address_data['country'])
16
16
  }
17
17
  end
@@ -32,7 +32,7 @@ module Spree
32
32
 
33
33
  def can_calculate_tax?(order)
34
34
  return false if order.flow_data.blank?
35
- return false if %w[cart address delivery].include?(order.state)
35
+ return false if %w[cart address].include?(order.state)
36
36
 
37
37
  true
38
38
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module FlowIoCreditCardDecorator
5
+ def self.prepended(base)
6
+ base.serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
7
+
8
+ base.store_accessor :meta, :flow_data
9
+ end
10
+
11
+ def push_authorization(auth_hash)
12
+ self.flow_data ||= {}
13
+ flow_data['authorizations'] ||= []
14
+ card_authorizations = flow_data['authorizations']
15
+ card_authorizations.delete_if { |ca| ca['id'] == auth_hash['id'] }
16
+ card_authorizations << auth_hash
17
+ end
18
+
19
+ Spree::CreditCard.prepend(self) if Spree::CreditCard.included_modules.exclude?(self)
20
+ end
21
+ end
@@ -1,44 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # `:display_total` modifications to display total prices beside Spree default. Example: https://i.imgur.com/7v2ix2G.png
4
- module Spree # rubocop:disable Metrics/ModuleLength
3
+ module Spree
5
4
  # Added flow specific methods to Spree::Order
6
- Order.class_eval do
7
- serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
5
+ module FlowIoOrderDecorator
6
+ def self.included(base)
7
+ base.serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
8
8
 
9
- store_accessor :meta, :flow_data
10
-
11
- before_save :sync_to_flow_io
12
- after_touch :sync_to_flow_io
9
+ base.store_accessor :meta, :flow_data
10
+ end
13
11
 
14
12
  def flow_tax_cache_key
15
13
  [number, 'flowcommerce', 'allocation', line_items.sum(:quantity)].join('-')
16
14
  end
17
15
 
18
- def sync_to_flow_io
19
- return unless zone&.flow_io_active_experience? && state == 'cart' && line_items.size > 0
20
-
21
- flow_io_order = FlowcommerceSpree::OrderSync.new(order: self)
22
- flow_io_order.build_flow_request
23
- flow_io_order.synchronize! if flow_data['digest'] != flow_io_order.digest
24
- end
25
-
26
16
  def display_total
27
- price = FlowcommerceSpree::Api.format_default_price total
28
- price += " (#{flow_total})" if flow_order
29
- price.html_safe
17
+ return unless flow_data&.[]('order')
18
+
19
+ Spree::Money.new(flow_io_total_amount, currency: currency)
30
20
  end
31
21
 
32
22
  def flow_order
33
- return unless flow_data&.[]('order')
34
-
35
- Hashie::Mash.new flow_data['order']
23
+ flow_data&.[]('order')
36
24
  end
37
25
 
38
26
  # accepts line item, usually called from views
39
27
  def flow_line_item_price(line_item, total = false)
40
- result = if flow_order
41
- item = flow_order.lines&.find { |el| el['item_number'] == line_item.variant.sku }
28
+ result = if (order = flow_order)
29
+ item = order['lines']&.find { |el| el['item_number'] == line_item.variant.sku }
42
30
 
43
31
  return 'n/a' unless item
44
32
 
@@ -95,10 +83,8 @@ module Spree # rubocop:disable Metrics/ModuleLength
95
83
  end
96
84
 
97
85
  # shows localized total, if possible. if not, fall back to Spree default
98
- def flow_total
99
- # r flow_order.total.label
100
- price = flow_order&.total&.label
101
- price || FlowcommerceSpree::Api.format_default_price(total)
86
+ def flow_io_total_amount
87
+ flow_data&.dig('order', 'total', 'amount')&.to_d
102
88
  end
103
89
 
104
90
  def flow_experience
@@ -108,10 +94,6 @@ module Spree # rubocop:disable Metrics/ModuleLength
108
94
  model.new ENV.fetch('FLOW_BASE_COUNTRY')
109
95
  end
110
96
 
111
- def flow_io_checkout_token
112
- flow_data&.[]('checkout_token')
113
- end
114
-
115
97
  def flow_io_experience_key
116
98
  flow_data&.[]('exp')
117
99
  end
@@ -124,19 +106,10 @@ module Spree # rubocop:disable Metrics/ModuleLength
124
106
  flow_data&.dig('order', 'id')
125
107
  end
126
108
 
127
- def flow_io_session_expires_at
128
- flow_data&.[]('session_expires_at')&.to_datetime
129
- end
130
-
131
109
  def flow_io_attributes
132
110
  flow_data&.dig('order', 'attributes') || {}
133
111
  end
134
112
 
135
- def add_flow_checkout_token(token)
136
- self.flow_data ||= {}
137
- self.flow_data['checkout_token'] = token
138
- end
139
-
140
113
  def flow_io_attribute_add(attr_key, value)
141
114
  self.flow_data['order'] ||= {}
142
115
  self.flow_data['order']['attributes'] ||= {}
@@ -153,35 +126,26 @@ module Spree # rubocop:disable Metrics/ModuleLength
153
126
  flow_data&.dig('order', 'attributes', 'user_uuid')
154
127
  end
155
128
 
156
- def checkout_url
157
- FlowcommerceSpree::OrderSync.new(order: self).synchronize!
158
-
159
- checkout_token = flow_io_checkout_token
160
- return "https://checkout.flow.io/tokens/#{checkout_token}" if checkout_token
129
+ def flow_io_captures
130
+ flow_data&.[]('captures')
161
131
  end
162
132
 
163
- # clear invalid zero amount payments. Solidus bug?
164
- def clear_zero_amount_payments!
165
- # class attribute that can be set to true
166
- return unless Flow::Order.clear_zero_amount_payments
167
-
168
- payments.where(amount: 0, state: %w[invalid processing pending]).map(&:destroy)
169
- end
133
+ def flow_io_captures_sum
134
+ captures_sum = 0
135
+ flow_data&.[]('captures')&.each do |c|
136
+ next if c['status'] != 'succeeded'
170
137
 
171
- def flow_order_authorized?
172
- flow_data&.[]('authorization') ? true : false
138
+ captures_sum += c['amount']
139
+ end
140
+ captures_sum.to_d
173
141
  end
174
142
 
175
- def flow_order_captured?
176
- flow_data['capture'] ? true : false
143
+ def flow_io_balance_amount
144
+ flow_data&.dig('order', 'balance', 'amount')&.to_d
177
145
  end
178
146
 
179
- # completes order and sets all states to finalized and complete
180
- # used when we have confirmed capture from Flow API or PayPal
181
- def flow_finalize!
182
- finalize! unless state == 'complete'
183
- update_column :payment_state, 'paid' if payment_state != 'paid'
184
- update_column :state, 'complete' if state != 'complete'
147
+ def flow_io_payments
148
+ flow_data.dig('order', 'payments')
185
149
  end
186
150
 
187
151
  def flow_payment_method
@@ -228,17 +192,19 @@ module Spree # rubocop:disable Metrics/ModuleLength
228
192
  s_address = flow_ship_address
229
193
 
230
194
  if s_address&.changes&.any?
231
- s_address.save
195
+ s_address.save!
232
196
  address_attributes[:ship_address_id] = s_address.id unless ship_address_id
233
197
  end
234
198
 
235
199
  b_address = flow_bill_address
236
200
  if b_address&.changes&.any?
237
- b_address.save
201
+ b_address.save!
238
202
  address_attributes[:bill_address_id] = b_address.id unless bill_address_id
239
203
  end
240
204
 
241
205
  address_attributes
242
206
  end
207
+
208
+ Spree::Order.include(self) if Spree::Order.included_modules.exclude?(self)
243
209
  end
244
210
  end
@@ -5,6 +5,8 @@
5
5
  module Spree
6
6
  class Gateway
7
7
  class FlowIo < Gateway
8
+ REFUND_SUCCESS = 'succeeded'
9
+
8
10
  def provider_class
9
11
  self.class
10
12
  end
@@ -23,7 +25,7 @@ module Spree
23
25
  end
24
26
 
25
27
  def method_type
26
- 'gateway'
28
+ 'flow_io_gateway'
27
29
  end
28
30
 
29
31
  def preferences
@@ -40,34 +42,29 @@ module Spree
40
42
  order.cc_authorization
41
43
  end
42
44
 
43
- def capture(_amount, _payment_method, options = {})
44
- order = load_order options
45
- order.cc_capture
46
- end
47
-
48
- def purchase(_amount, _payment_method, options = {})
49
- order = load_order options
50
- flow_auth = order.cc_authorization
51
-
52
- if flow_auth.success?
53
- order.cc_capture
54
- else
55
- flow_auth
56
- end
45
+ def refund(payment, amount, _options = {})
46
+ request_refund_store_result(payment.order, amount)
47
+ rescue StandardError => e
48
+ ActiveMerchant::Billing::Response.new(false, e.to_s, {}, {})
57
49
  end
58
50
 
59
- def refund(_money, _authorization_key, options = {})
60
- order = load_order options
61
- order.cc_refund
51
+ def cancel(authorization)
52
+ original_payment = Spree::Payment.find_by(response_code: authorization)
53
+ request_refund_store_result(original_payment.order, original_payment.amount)
54
+ rescue StandardError => e
55
+ ActiveMerchant::Billing::Response.new(false, e.to_s, {}, {})
62
56
  end
63
57
 
64
- def void(money, authorization_key, options = {})
65
- # binding.pry
58
+ def void(authorization_id, _source, options = {})
59
+ amount = (options[:subtotal] + options[:shipping]) * 0.01
60
+ reversal_form = Io::Flow::V0::Models::ReversalForm.new(key: options[:order_id],
61
+ authorization_id: authorization_id,
62
+ amount: amount,
63
+ currency: options[:currency])
64
+ FlowcommerceSpree.client.reversals.post(FlowcommerceSpree::ORGANIZATION, reversal_form)
66
65
  end
67
66
 
68
67
  def create_profile(payment)
69
- # binding.pry
70
-
71
68
  # payment.order.state
72
69
  @credit_card = payment.source
73
70
 
@@ -77,12 +74,52 @@ module Spree
77
74
 
78
75
  private
79
76
 
77
+ def request_refund_store_result(order, amount)
78
+ refund_form = Io::Flow::V0::Models::RefundForm.new(order_number: order.number,
79
+ amount: amount,
80
+ currency: order.currency)
81
+ response = FlowcommerceSpree.client.refunds.post(FlowcommerceSpree::ORGANIZATION, refund_form)
82
+ response_status = response.status.value
83
+ if response_status == REFUND_SUCCESS
84
+ add_refund_to_order(response, order)
85
+ map_refund_to_payment(response, order)
86
+ ActiveMerchant::Billing::Response.new(true, REFUND_SUCCESS, {}, {})
87
+ else
88
+ msg = "Partial refund fail. Details: #{response_status}"
89
+ ActiveMerchant::Billing::Response.new(false, msg, {}, {})
90
+ end
91
+ end
92
+
93
+ def add_refund_to_order(response, order)
94
+ order.flow_data ||= {}
95
+ order.flow_data['refunds'] ||= []
96
+ order_refunds = order.flow_data['refunds']
97
+ order_refunds.delete_if { |r| r['id'] == response.id }
98
+ order_refunds << response.to_hash
99
+ order.update_column(:meta, order.meta.to_json)
100
+ end
101
+
102
+ def map_refund_to_payment(response, order)
103
+ original_payment = Spree::Payment.find_by(response_code: response.authorization.id)
104
+ payment = order.payments.create!(state: 'completed',
105
+ response_code: response.authorization.id,
106
+ payment_method_id: original_payment&.payment_method_id,
107
+ amount: - response.amount,
108
+ source_id: original_payment&.source_id,
109
+ source_type: original_payment&.source_type)
110
+
111
+ # For now this additional update is overwriting the generated identifier with flow.io payment identifier.
112
+ # TODO: Check and possibly refactor in Spree 3.0, where the `before_create :set_unique_identifier`
113
+ # has been removed.
114
+ payment.update_column(:identifier, response.id)
115
+ end
116
+
80
117
  # hard inject Flow as payment method unless defined
81
118
  def profile_ensure_payment_method_is_present!
82
119
  return if @credit_card.payment_method_id
83
120
 
84
- flow_payment = Spree::PaymentMethod.where(active: true, type: 'Spree::Gateway::FlowIo').first
85
- @credit_card.payment_method_id = flow_payment.id if flow_payment
121
+ flow_payment_method = Spree::PaymentMethod.find_by(active: true, type: 'Spree::Gateway::FlowIo')
122
+ @credit_card.payment_method_id = flow_payment_method.id if flow_payment_method
86
123
  end
87
124
 
88
125
  # create payment profile with Flow and tokenize Credit Card