flowcommerce_spree 0.0.4 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -6
  3. data/app/controllers/concerns/current_zone_loader_decorator.rb +7 -12
  4. data/app/controllers/flowcommerce_spree/orders_controller.rb +3 -1
  5. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +16 -18
  6. data/app/models/flowcommerce_spree/settings.rb +1 -0
  7. data/app/models/spree/calculator/flow_io.rb +23 -11
  8. data/app/models/spree/calculator/shipping/flow_io.rb +5 -2
  9. data/app/models/spree/flow_io_order_decorator.rb +29 -58
  10. data/app/models/spree/flow_io_product_decorator.rb +5 -0
  11. data/app/models/spree/flow_io_variant_decorator.rb +16 -6
  12. data/app/models/spree/gateway/flow_io.rb +22 -11
  13. data/app/models/spree/zones/flow_io_product_zone_decorator.rb +4 -0
  14. data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
  15. data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
  16. data/app/services/flowcommerce_spree/import_experience_items.rb +0 -20
  17. data/app/services/flowcommerce_spree/import_item.rb +45 -0
  18. data/app/services/flowcommerce_spree/order_sync.rb +39 -82
  19. data/app/services/flowcommerce_spree/order_updater.rb +3 -1
  20. data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
  21. data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
  22. data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
  23. data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
  24. data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
  25. data/app/workers/flowcommerce_spree/import_item_worker.rb +24 -0
  26. data/config/routes.rb +1 -1
  27. data/lib/flowcommerce_spree.rb +3 -1
  28. data/lib/flowcommerce_spree/engine.rb +5 -0
  29. data/lib/flowcommerce_spree/experience_service.rb +1 -27
  30. data/lib/flowcommerce_spree/session.rb +5 -7
  31. data/lib/flowcommerce_spree/version.rb +1 -1
  32. data/lib/tasks/flowcommerce_spree.rake +4 -1
  33. metadata +74 -16
  34. data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
  35. data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
  36. data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
  37. data/lib/flow/error.rb +0 -73
  38. data/lib/flow/pay_pal.rb +0 -25
  39. data/lib/flowcommerce_spree/webhook_service.rb +0 -154
  40. data/lib/simple_csv_writer.rb +0 -44
@@ -21,7 +21,7 @@ module Spree
21
21
  end
22
22
 
23
23
  def payment_profiles_supported?
24
- true
24
+ false
25
25
  end
26
26
 
27
27
  def method_type
@@ -43,14 +43,18 @@ module Spree
43
43
  end
44
44
 
45
45
  def refund(payment, amount, _options = {})
46
- request_refund_store_result(payment.order, amount)
46
+ response = request_refund_store_result(payment.order, amount)
47
+ map_refund_to_payment(response, payment.order) if response.success?
48
+ response
47
49
  rescue StandardError => e
48
50
  ActiveMerchant::Billing::Response.new(false, e.to_s, {}, {})
49
51
  end
50
52
 
51
53
  def cancel(authorization)
52
54
  original_payment = Spree::Payment.find_by(response_code: authorization)
53
- request_refund_store_result(original_payment.order, original_payment.amount)
55
+ response = request_refund_store_result(original_payment.order, original_payment.amount)
56
+ map_refund_to_payment(response, original_payment.order) if response.success?
57
+ response
54
58
  rescue StandardError => e
55
59
  ActiveMerchant::Billing::Response.new(false, e.to_s, {}, {})
56
60
  end
@@ -72,6 +76,12 @@ module Spree
72
76
  create_flow_cc_profile!
73
77
  end
74
78
 
79
+ def credit(payment, credit_amount)
80
+ request_refund_store_result(payment.order, credit_amount)
81
+ rescue StandardError => e
82
+ ActiveMerchant::Billing::Response.new(false, e.to_s, {}, {})
83
+ end
84
+
75
85
  private
76
86
 
77
87
  def request_refund_store_result(order, amount)
@@ -82,8 +92,10 @@ module Spree
82
92
  response_status = response.status.value
83
93
  if response_status == REFUND_SUCCESS
84
94
  add_refund_to_order(response, order)
85
- map_refund_to_payment(response, order)
86
- ActiveMerchant::Billing::Response.new(true, REFUND_SUCCESS, {}, {})
95
+ ActiveMerchant::Billing::Response.new(true,
96
+ REFUND_SUCCESS,
97
+ response.to_hash,
98
+ authorization: response.authorization.id)
87
99
  else
88
100
  msg = "Partial refund fail. Details: #{response_status}"
89
101
  ActiveMerchant::Billing::Response.new(false, msg, {}, {})
@@ -100,18 +112,17 @@ module Spree
100
112
  end
101
113
 
102
114
  def map_refund_to_payment(response, order)
103
- original_payment = Spree::Payment.find_by(response_code: response.authorization.id)
115
+ original_payment = Spree::Payment.find_by(response_code: response.authorization)
104
116
  payment = order.payments.create!(state: 'completed',
105
- response_code: response.authorization.id,
117
+ response_code: response.authorization,
106
118
  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)
119
+ amount: - response.params['amount'].to_f,
120
+ source: original_payment)
110
121
 
111
122
  # For now this additional update is overwriting the generated identifier with flow.io payment identifier.
112
123
  # TODO: Check and possibly refactor in Spree 3.0, where the `before_create :set_unique_identifier`
113
124
  # has been removed.
114
- payment.update_column(:identifier, response.id)
125
+ payment.update_column(:identifier, response.params['id'])
115
126
  end
116
127
 
117
128
  # hard inject Flow as payment method unless defined
@@ -28,6 +28,10 @@ module Spree
28
28
  flow_data&.[]('key').present? && flow_data['status'] == 'active'
29
29
  end
30
30
 
31
+ def flow_io_active_or_archiving_experience?
32
+ flow_data&.[]('key').present? && %w[active archiving].include?(flow_data['status'])
33
+ end
34
+
31
35
  def update_on_flow; end
32
36
 
33
37
  def remove_on_flow_io
@@ -0,0 +1,13 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/shared/_order_tabs',
3
+ name: 'spree_admin_order_additional_information_flow_message',
4
+ insert_top: '.additional-info',
5
+ text: '
6
+ <% if FlowcommerceSpree::ORGANIZATION.present? && @order.flow_order.present? %>
7
+ <div style="text-align: center">
8
+ <%= link_to "See Flow Order",
9
+ "https://console.flow.io/#{FlowcommerceSpree::ORGANIZATION}/orders/#{@order.number}",
10
+ target: "_blank", class: "button" %>
11
+ </div>
12
+ <% end %>'
13
+ )
@@ -0,0 +1,9 @@
1
+ Deface::Override.new(
2
+ virtual_path: 'spree/admin/prices/index',
3
+ name: 'spree_admin_prices_flow_mesage',
4
+ insert_top: '.no-border-top',
5
+ text: "
6
+ <div class='spree-admin-info' >
7
+ To check localized pricing, please click <a href='#{"https://console.flow.io/#{ENV['FLOW_ORGANIZATION']}/price-books"}' target='_blank'>here</a>.
8
+ </div>"
9
+ )
@@ -33,14 +33,6 @@ module FlowcommerceSpree
33
33
  item_hash = item.to_hash
34
34
  next unless (variant = Spree::Variant.find_by(sku: item_hash.delete(:number)))
35
35
 
36
- status_in_experience = item_hash.dig(:local, :status)
37
-
38
- if status_in_experience != 'included'
39
- log_str << "[#{status_in_experience.red}]:"
40
- else # If at least a variant is included in experience, include the product too
41
- adjust_product_zone(variant)
42
- end
43
-
44
36
  variant.flow_import_item(item_hash, experience_key: @experience_key)
45
37
 
46
38
  log_str << "#{variant.sku}, "
@@ -60,17 +52,5 @@ module FlowcommerceSpree
60
52
  @organization = organization
61
53
  @zone = zone
62
54
  end
63
-
64
- def adjust_product_zone(variant)
65
- return unless (product = variant.product)
66
-
67
- zone_ids = product.zone_ids || []
68
- zone_id_string = @zone.id.to_s
69
- return if zone_ids.include?(zone_id_string)
70
-
71
- zone_ids << zone_id_string
72
- product.zone_ids = zone_ids
73
- product.update_columns(meta: product.meta.to_json)
74
- end
75
55
  end
76
56
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ # A service object to import the data for product variants belonging to a flow.io Experience
5
+ class ImportItem
6
+ def self.run(variant, client: FlowcommerceSpree.client, organization: ORGANIZATION)
7
+ new(variant, client: client, organization: organization).run
8
+ end
9
+
10
+ def run
11
+ @client.experiences.get(@organization, status: 'active').each do |experience|
12
+ experience_key = experience.key
13
+ zone = Spree::Zones::Product.find_by(name: experience_key.titleize)
14
+ next unless zone
15
+
16
+ import_data(zone)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def initialize(variant, client:, organization:)
23
+ @client = client
24
+ @logger = client.instance_variable_get(:@http_handler).logger
25
+ @organization = organization
26
+ @variant = variant
27
+ end
28
+
29
+ def import_data(zone)
30
+ experience_key = zone.flow_io_experience
31
+ item = begin
32
+ @client.experiences.get_items_by_number(@organization, @variant.sku, experience: experience_key)
33
+ rescue Io::Flow::V0::HttpClient::PreconditionException, Io::Flow::V0::HttpClient::ServerError => e
34
+ @logger.info "flow.io API error: #{e.message}"
35
+ end
36
+ return unless item
37
+
38
+ item_hash = item.to_hash
39
+
40
+ @variant.flow_import_item(item_hash, experience_key: @experience_key)
41
+
42
+ @logger.info "[#{@variant.sku}][#{experience_key}] Variant experience imported successfully."
43
+ end
44
+ end
45
+ end
@@ -2,58 +2,40 @@
2
2
 
3
3
  module FlowcommerceSpree
4
4
  # represents flow.io order syncing service
5
- # for easy integration we are currently passing:
6
- # - flow experience
7
- # - spree order
8
- # - current customer, if present as @order.user
9
- #
10
- # example:
11
- # flow_order = FlowcommerceSpree::OrderSync.new # init flow-order object
12
- # order: Spree::Order.last,
13
- # experience: @flow_session.experience
14
- # customer: @order.user
15
- # flow_order.build_flow_request # builds json body to be posted to flow.io api
16
- # flow_order.synchronize! # sends order to flow
17
- class OrderSync # rubocop:disable Metrics/ClassLength
5
+ class OrderSync
18
6
  FLOW_CENTER = 'default'
19
7
 
20
8
  attr_reader :order, :response
21
9
 
22
- delegate :url_helpers, to: 'Rails.application.routes'
23
-
10
+ # @param [Object] order
11
+ # @param [String] flow_session_id
24
12
  def initialize(order:, flow_session_id:)
25
- raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
13
+ raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
26
14
 
27
15
  @experience = order.flow_io_experience_key
28
16
  @flow_session_id = flow_session_id
29
17
  @order = order
30
- @client = FlowcommerceSpree.client(session_id: flow_session_id)
18
+ @client = FlowcommerceSpree.client(default_headers: { "Authorization": "Session #{flow_session_id}" },
19
+ authorization: nil)
31
20
  end
32
21
 
33
22
  # helper method to send complete order from Spree to flow.io
34
23
  def synchronize!
35
- return unless @order.zone&.flow_io_active_experience? && @order.state == 'cart' && @order.line_items.size > 0
24
+ return unless @order.state == 'cart' && @order.line_items.size > 0
36
25
 
37
26
  sync_body!
38
- write_response_in_cache
27
+ write_response_to_order
39
28
 
40
29
  @order.update_columns(total: @order.total, meta: @order.meta.to_json)
41
30
  refresh_checkout_token
42
- @checkout_token
43
- end
44
-
45
- def error
46
- @response['messages'].join(', ')
47
- end
48
-
49
- def error_code
50
- @response['code']
51
31
  end
52
32
 
53
33
  def error?
54
34
  @response&.[]('code') && @response&.[]('messages') ? true : false
55
35
  end
56
36
 
37
+ private
38
+
57
39
  # builds object that can be sent to api.flow.io to sync order data
58
40
  def build_flow_request
59
41
  @opts = { experience: @experience, expand: ['experience'] }
@@ -71,24 +53,21 @@ module FlowcommerceSpree
71
53
  @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency } if @order.adjustment_total != 0
72
54
  end
73
55
 
74
- private
75
-
76
56
  def refresh_checkout_token
77
- root_url = url_helpers.root_url
57
+ root_url = Rails.application.routes.url_helpers.root_url
78
58
  order_number = @order.number
79
59
  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(
81
- FlowcommerceSpree::ORGANIZATION,
82
- discriminator: 'checkout_token_reference_form',
83
- order_number: order_number,
84
- session_id: @flow_session_id,
85
- urls: { continue_shopping: root_url,
86
- confirmation: confirmation_url,
87
- invalid_checkout: root_url }
88
- )&.id
89
-
90
60
  @order.flow_io_attribute_add('flow_return_url', confirmation_url)
91
61
  @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
62
+
63
+ FlowcommerceSpree.client.checkout_tokens.post_checkout_and_tokens_by_organization(
64
+ FlowcommerceSpree::ORGANIZATION, discriminator: 'checkout_token_reference_form',
65
+ order_number: order_number,
66
+ session_id: @flow_session_id,
67
+ urls: { continue_shopping: root_url,
68
+ confirmation: confirmation_url,
69
+ invalid_checkout: root_url }
70
+ )&.id
92
71
  end
93
72
 
94
73
  # if customer is defined, add customer info
@@ -100,13 +79,14 @@ module FlowcommerceSpree
100
79
  customer_ship_address = customer.ship_address
101
80
  address = customer_ship_address if customer_ship_address&.country&.iso3 == @order.zone.flow_io_experience_country
102
81
 
82
+ customer_profile = customer.user_profile
103
83
  unless address
104
- user_profile_address = customer.user_profile&.address
84
+ user_profile_address = customer_profile&.address
105
85
  address = user_profile_address if user_profile_address&.country&.iso3 == @order.zone.flow_io_experience_country
106
86
  end
107
87
 
108
- @body[:customer] = { name: { first: address&.firstname,
109
- last: address&.lastname },
88
+ @body[:customer] = { name: { first: address&.firstname || customer_profile&.first_name,
89
+ last: address&.lastname || customer_profile&.last_name },
110
90
  email: customer.email,
111
91
  number: customer.flow_number,
112
92
  phone: address&.phone }
@@ -116,14 +96,14 @@ module FlowcommerceSpree
116
96
 
117
97
  def add_customer_address(address)
118
98
  streets = []
119
- streets.push address.address1 if address&.address1.present?
120
- streets.push address.address2 if address&.address2.present?
99
+ streets.push address.address1 if address.address1.present?
100
+ streets.push address.address2 if address.address2.present?
121
101
 
122
102
  @body[:destination] = { streets: streets,
123
- city: address&.city,
124
- province: address&.state_name,
125
- postal: address&.zipcode,
126
- country: (address&.country&.iso3 || ''),
103
+ city: address.city,
104
+ province: address.state_name,
105
+ postal: address.zipcode,
106
+ country: (address.country&.iso3 || ''),
127
107
  contact: @body[:customer] }
128
108
 
129
109
  @body[:destination].delete_if { |_k, v| v.nil? }
@@ -132,20 +112,8 @@ module FlowcommerceSpree
132
112
  def sync_body!
133
113
  build_flow_request
134
114
 
135
- @use_get = false
136
-
137
- # use get if order is completed and closed
138
- @use_get = true if @order.flow_data.dig('order', 'submitted_at').present? || @order.state == 'complete'
139
-
140
- # do not use get if there is no local order cache
141
- @use_get = false unless @order.flow_data['order']
142
-
143
- if @use_get
144
- @response ||= @client.orders.get_by_number(ORGANIZATION, @order.number).to_hash
145
- else
146
- @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
147
- Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
148
- end
115
+ @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
116
+ Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
149
117
  end
150
118
 
151
119
  def add_item(line_item)
@@ -156,27 +124,16 @@ module FlowcommerceSpree
156
124
  { center: FLOW_CENTER,
157
125
  number: variant.sku,
158
126
  quantity: line_item.quantity,
159
- price: { amount: price_root['amount'] || variant.cost_price,
127
+ price: { amount: price_root['amount'] || variant.price,
160
128
  currency: price_root['currency'] || variant.cost_currency } }
161
129
  end
162
130
 
163
- # set cache for total order amount
164
- # written in flow_data field inside spree_orders table
165
- def write_response_in_cache
166
- if !@response || error?
167
- @order.flow_data.delete('order')
168
- else
169
- response_total = @response[:total]
170
- response_total_label = response_total&.[](:label)
171
- cache_total = @order.flow_data.dig('order', 'total', 'label')
172
-
173
- # return if total is not changed, no products removed or added
174
- return if @use_get && response_total_label == cache_total
175
-
176
- # update local order
177
- @order.total = response_total&.[](:amount)
178
- @order.flow_data.merge!('order' => @response)
179
- end
131
+ def write_response_to_order
132
+ return @order.flow_data.delete('order') if !@response || error?
133
+
134
+ # update local order
135
+ @order.total = @response[:total]&.[](:amount)
136
+ @order.flow_data.merge!('order' => @response)
180
137
  end
181
138
  end
182
139
  end
@@ -3,7 +3,9 @@
3
3
  module FlowcommerceSpree
4
4
  class OrderUpdater
5
5
  def initialize(order:)
6
- raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
6
+ unless order&.zone&.flow_io_active_or_archiving_experience?
7
+ raise(ArgumentError, 'Experience not defined or not active')
8
+ end
7
9
 
8
10
  @experience = order.flow_io_experience_key
9
11
  @order = order
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ module Webhooks
5
+ class CaptureUpsertedV2
6
+ attr_reader :errors
7
+ alias full_messages errors
8
+
9
+ def self.process(data)
10
+ new(data).process
11
+ end
12
+
13
+ def initialize(data)
14
+ @data = data
15
+ @errors = []
16
+ end
17
+
18
+ def process
19
+ errors << { message: 'Capture param missing' } && (return self) unless (capture = @data['capture']&.to_hash)
20
+
21
+ order_number = capture.dig('authorization', 'order', 'number')
22
+ errors << { message: 'Order number param missing' } && (return self) unless order_number
23
+
24
+ if (order = Spree::Order.find_by(number: order_number))
25
+ upsert_order_captures(order, capture)
26
+ payments = order.flow_io_payments
27
+ map_payment_captures_to_spree(order, payments) if payments.present?
28
+ order
29
+ else
30
+ errors << { message: "Order #{order_number} not found" }
31
+ self
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def upsert_order_captures(order, capture)
38
+ order.flow_data ||= {}
39
+ order.flow_data['captures'] ||= []
40
+ order_captures = order.flow_data['captures']
41
+ order_captures.delete_if { |c| c['id'] == capture['id'] }
42
+ order_captures << capture
43
+ order.update_column(:meta, order.meta.to_json)
44
+ end
45
+
46
+ def map_payment_captures_to_spree(order, payments)
47
+ order.flow_data['captures']&.each do |c|
48
+ next unless (payment = captured_payment(payments, c))
49
+
50
+ payment.capture_events.create!(amount: c['amount'], meta: { 'flow_data' => { 'id' => c['id'] } })
51
+ return if payment.completed? || payment.capture_events.sum(:amount) < payment.amount
52
+
53
+ payment.complete
54
+ end
55
+
56
+ return if order.completed?
57
+ return unless order.flow_io_captures_sum >= order.flow_io_total_amount && order.flow_io_balance_amount <= 0
58
+
59
+ FlowcommerceSpree::OrderUpdater.new(order: order).finalize_order
60
+ end
61
+
62
+ def captured_payment(flow_order_payments, capture)
63
+ return unless capture['status'] == 'succeeded'
64
+
65
+ auth = capture.dig('authorization', 'id')
66
+ return unless flow_order_payments&.find { |p| p['reference'] == auth }
67
+
68
+ return unless (payment = Spree::Payment.find_by(response_code: auth))
69
+
70
+ return if Spree::PaymentCaptureEvent.where("meta -> 'flow_data' ->> 'id' = ?", capture['id']).exists?
71
+
72
+ payment
73
+ end
74
+ end
75
+ end
76
+ end