flowcommerce_spree 0.0.4 → 0.0.9

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