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