flowcommerce_spree 0.0.2 → 0.0.7

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -13
  3. data/SPREE_FLOW.md +6 -28
  4. data/app/controllers/concerns/current_zone_loader_decorator.rb +33 -25
  5. data/app/controllers/flowcommerce_spree/inventory_controller.rb +23 -0
  6. data/app/controllers/flowcommerce_spree/orders_controller.rb +20 -0
  7. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +23 -13
  8. data/app/controllers/users/sessions_controller_decorator.rb +28 -0
  9. data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +4 -9
  10. data/app/models/spree/address_decorator.rb +19 -0
  11. data/app/models/spree/calculator/flow_io.rb +61 -0
  12. data/app/models/spree/calculator/shipping/flow_io.rb +40 -0
  13. data/app/models/spree/flow_io_credit_card_decorator.rb +21 -0
  14. data/app/models/spree/flow_io_order_decorator.rb +163 -0
  15. data/app/models/spree/flow_io_variant_decorator.rb +4 -2
  16. data/app/models/spree/gateway/flow_io.rb +153 -0
  17. data/app/models/spree/{credit_card_decorator.rb → payment_capture_event_decorator.rb} +1 -1
  18. data/app/models/spree/promotion_handler/coupon_decorator.rb +1 -1
  19. data/app/models/spree/zones/flow_io_product_zone_decorator.rb +8 -0
  20. data/app/models/tracking/setup_decorator.rb +40 -0
  21. data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
  22. data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
  23. data/app/serializers/api/v2/order_serializer_decorator.rb +20 -0
  24. data/app/services/flowcommerce_spree/import_experience_items.rb +1 -1
  25. data/app/services/flowcommerce_spree/order_sync.rb +81 -173
  26. data/app/services/flowcommerce_spree/order_updater.rb +78 -0
  27. data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
  28. data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
  29. data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
  30. data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
  31. data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
  32. data/app/views/spree/admin/payments/source_views/_flow_io_gateway.html.erb +21 -0
  33. data/config/rails_best_practices.yml +51 -0
  34. data/config/routes.rb +3 -1
  35. data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +6 -4
  36. data/lib/flow/simple_gateway.rb +0 -36
  37. data/lib/flowcommerce_spree.rb +17 -3
  38. data/lib/flowcommerce_spree/engine.rb +33 -3
  39. data/lib/flowcommerce_spree/experience_service.rb +1 -27
  40. data/lib/flowcommerce_spree/logging_http_client.rb +33 -15
  41. data/lib/flowcommerce_spree/session.rb +17 -32
  42. data/lib/flowcommerce_spree/test_support.rb +7 -0
  43. data/lib/flowcommerce_spree/version.rb +1 -1
  44. data/lib/tasks/flowcommerce_spree.rake +4 -1
  45. metadata +88 -21
  46. data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
  47. data/app/models/spree/gateway/spree_flow_gateway.rb +0 -116
  48. data/app/models/spree/line_item_decorator.rb +0 -15
  49. data/app/models/spree/order_decorator.rb +0 -179
  50. data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
  51. data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
  52. data/config/initializers/flowcommerce_spree.rb +0 -7
  53. data/lib/flow/error.rb +0 -73
  54. data/lib/flow/pay_pal.rb +0 -25
  55. data/lib/flowcommerce_spree/webhook_service.rb +0 -98
  56. data/lib/simple_csv_writer.rb +0 -44
@@ -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
+ )
@@ -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
 
@@ -2,196 +2,118 @@
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, present as @current_spree_user controller instance variable
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: Spree::User.last
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
5
  class OrderSync
18
6
  FLOW_CENTER = 'default'
19
7
 
20
- attr_reader :digest, :order, :response
8
+ attr_reader :order, :response
21
9
 
22
- class << self
23
- def clear_cache(order)
24
- return unless order.flow_data['order']
10
+ # @param [Object] order
11
+ # @param [String] flow_session_id
12
+ def initialize(order:, flow_session_id:)
13
+ raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
25
14
 
26
- order.flow_data.delete('order')
27
- order.update_column :meta, order.meta.to_json
28
- end
29
- end
30
-
31
- def initialize(order:)
32
- raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
33
-
34
- @client = FlowcommerceSpree.client(session_id: order.flow_data['session_id'])
35
15
  @experience = order.flow_io_experience_key
36
- @order = order
37
- @customer = order.user
38
- @items = []
16
+ @flow_session_id = flow_session_id
17
+ @order = order
18
+ @client = FlowcommerceSpree.client(default_headers: { "Authorization": "Session #{flow_session_id}" },
19
+ authorization: nil)
39
20
  end
40
21
 
41
22
  # helper method to send complete order from Spree to flow.io
42
23
  def synchronize!
43
- sync_body!
44
- check_state!
45
- write_response_in_cache
46
- @response
47
- end
24
+ return unless @order.state == 'cart' && @order.line_items.size > 0
48
25
 
49
- def error
50
- @response['messages'].join(', ')
51
- end
26
+ sync_body!
27
+ write_response_to_order
52
28
 
53
- def error_code
54
- @response['code']
29
+ @order.update_columns(total: @order.total, meta: @order.meta.to_json)
30
+ refresh_checkout_token
55
31
  end
56
32
 
57
33
  def error?
58
34
  @response&.[]('code') && @response&.[]('messages') ? true : false
59
35
  end
60
36
 
61
- def delivery
62
- deliveries.select { |el| el[:active] }.first
63
- end
64
-
65
- # delivery methods are defined in flow console
66
- def deliveries
67
- # if we have erorr with an order, but still using this method
68
- return [] unless @order.flow_order
69
-
70
- @order.flow_data ||= {}
71
-
72
- delivery_list = @order.flow_order['deliveries'][0]['options']
73
- delivery_list = delivery_list.map do |opts|
74
- name = opts['tier']['name']
75
-
76
- # add original Flow ID
77
- # name += ' (%s)' % opts['tier']['strategy'] if opts['tier']['strategy']
78
-
79
- selection_id = opts['id']
80
-
81
- { id: selection_id,
82
- price: { label: opts['price']['label'] },
83
- active: @order.flow_order['selections'].include?(selection_id),
84
- name: name }
85
- end.to_a
86
-
87
- # make first one active unless we have active element
88
- delivery_list.first[:active] = true unless delivery_list.select { |el| el[:active] }.first
89
-
90
- delivery_list
91
- end
92
-
93
- def total_price
94
- @order.flow_total
95
- end
96
-
97
- def delivered_duty
98
- # paid is default
99
- @order.flow_data['delivered_duty'] || ::Io::Flow::V0::Models::DeliveredDuty.paid.value
100
- end
37
+ private
101
38
 
102
39
  # builds object that can be sent to api.flow.io to sync order data
103
40
  def build_flow_request
104
- @order.line_items.each { |line_item| add_item(line_item) }
41
+ @opts = { experience: @experience, expand: ['experience'] }
42
+ @body = { items: @order.line_items.map { |line_item| add_item(line_item) } }
105
43
 
106
- @opts = {}
107
- @opts[:experience] = @experience
108
- @opts[:expand] = ['experience']
44
+ try_to_add_customer
109
45
 
110
- @body = { items: @items, number: @order.number }
46
+ return unless (flow_data = @order.flow_data['order'])
111
47
 
112
- add_customer if @customer
48
+ @body[:selections] = flow_data['selections'].presence
49
+ @body[:delivered_duty] = flow_data['delivered_duty'].presence
50
+ @body[:attributes] = flow_data['attributes'].presence
113
51
 
114
- if (flow_data = @order.flow_data['order'])
115
- @body[:selections] = flow_data['selections'].presence
116
- @body[:delivered_duty] = flow_data['delivered_duty'].presence
117
- @body[:attributes] = flow_data['attributes'].presence
52
+ # discount on full order is applied
53
+ @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency } if @order.adjustment_total != 0
54
+ end
118
55
 
119
- if @order.adjustment_total != 0
120
- # discount on full order is applied
121
- @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency }
122
- end
123
- end
56
+ def refresh_checkout_token
57
+ root_url = Rails.application.routes.url_helpers.root_url
58
+ order_number = @order.number
59
+ confirmation_url = "#{root_url}flow/order-completed?order=#{order_number}&t=#{@order.guest_token}"
60
+ @order.flow_io_attribute_add('flow_return_url', confirmation_url)
61
+ @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
124
62
 
125
- # calculate digest body and cache it
126
- @digest = Digest::SHA1.hexdigest(@opts.to_json + @body.to_json)
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
127
71
  end
128
72
 
129
- private
130
-
131
73
  # if customer is defined, add customer info
132
74
  # it is possible to have order in Spree without customer info (new guest session)
133
- def add_customer
134
- return unless @customer
135
-
136
- address = @customer.ship_address
137
- # address = nil
138
- if address
139
- @body[:customer] = { name: { first: address.firstname,
140
- last: address.lastname },
141
- email: @customer.email,
142
- number: @customer.flow_number,
143
- phone: address.phone }
144
-
145
- streets = []
146
- streets.push address.address1 unless address.address1.blank?
147
- streets.push address.address2 unless address.address2.blank?
148
-
149
- @body[:destination] = { streets: streets,
150
- city: address.city,
151
- province: address.state_name,
152
- postal: address.zipcode,
153
- country: (address.country.iso3 || 'USA'),
154
- contact: @body[:customer] }
155
-
156
- @body[:destination].delete_if { |_k, v| v.nil? }
157
- end
75
+ def try_to_add_customer
76
+ return unless (customer = @order.user)
158
77
 
159
- @body
160
- end
78
+ address = nil
79
+ customer_ship_address = customer.ship_address
80
+ address = customer_ship_address if customer_ship_address&.country&.iso3 == @order.zone.flow_io_experience_country
161
81
 
162
- def sync_body!
163
- build_flow_request if @body.blank?
82
+ customer_profile = customer.user_profile
83
+ unless address
84
+ user_profile_address = customer_profile&.address
85
+ address = user_profile_address if user_profile_address&.country&.iso3 == @order.zone.flow_io_experience_country
86
+ end
164
87
 
165
- @use_get = false
88
+ @body[:customer] = { name: { first: address&.firstname || customer_profile&.first_name,
89
+ last: address&.lastname || customer_profile&.last_name },
90
+ email: customer.email,
91
+ number: customer.flow_number,
92
+ phone: address&.phone }
166
93
 
167
- # use get if order is completed and closed
168
- @use_get = true if @order.state == 'complete'
94
+ add_customer_address(address) if address
95
+ end
169
96
 
170
- # use get if local digest hash check said there is no change
171
- @use_get ||= true if @order.flow_data['digest'] == @digest
97
+ def add_customer_address(address)
98
+ streets = []
99
+ streets.push address.address1 if address.address1.present?
100
+ streets.push address.address2 if address.address2.present?
172
101
 
173
- # do not use get if there is no local order cache
174
- @use_get = false unless @order.flow_data['order']
102
+ @body[:destination] = { streets: streets,
103
+ city: address.city,
104
+ province: address.state_name,
105
+ postal: address.zipcode,
106
+ country: (address.country&.iso3 || ''),
107
+ contact: @body[:customer] }
175
108
 
176
- if @use_get
177
- @response ||= FlowcommerceSpree::Api.run :get, "/:organization/orders/#{@body[:number]}", expand: 'experience'
178
- else
179
- @response = @client.orders.put_by_number(FlowcommerceSpree::ORGANIZATION, @order.number,
180
- Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
181
- end
109
+ @body[:destination].delete_if { |_k, v| v.nil? }
182
110
  end
183
111
 
184
- def check_state!
185
- # authorize if not authorized
186
- # if !@order.flow_order_authorized?
187
-
188
- # authorize payment on complete, unless authorized
189
- if @order.state == 'complete' && !@order.flow_order_authorized?
190
- simple_gateway = Flow::SimpleGateway.new(@order)
191
- simple_gateway.cc_authorization
192
- end
112
+ def sync_body!
113
+ build_flow_request
193
114
 
194
- @order.flow_finalize! if @order.flow_order_authorized? && @order.state != 'complete'
115
+ @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
116
+ Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
195
117
  end
196
118
 
197
119
  def add_item(line_item)
@@ -199,33 +121,19 @@ module FlowcommerceSpree
199
121
  price_root = variant.flow_data&.dig('exp', @experience, 'prices')&.[](0) || {}
200
122
 
201
123
  # create flow order line item
202
- item = { center: FLOW_CENTER,
203
- number: variant.sku,
204
- quantity: line_item.quantity,
205
- price: { amount: price_root['amount'] || variant.cost_price,
206
- currency: price_root['currency'] || variant.cost_currency } }
207
-
208
- @items.push item
124
+ { center: FLOW_CENTER,
125
+ number: variant.sku,
126
+ quantity: line_item.quantity,
127
+ price: { amount: price_root['amount'] || variant.cost_price,
128
+ currency: price_root['currency'] || variant.cost_currency } }
209
129
  end
210
130
 
211
- # set cache for total order ammount
212
- # written in flow_data field inside spree_orders table
213
- def write_response_in_cache
214
- if !@response || error?
215
- @order.flow_data.delete('digest')
216
- @order.flow_data.delete('order')
217
- else
218
- response_total = @response.dig('total', 'label')
219
- cache_total = @order.flow_data.dig('order', 'total', 'label')
220
-
221
- # return if total is not changed, no products removed or added
222
- return if @use_get && response_total == cache_total
223
-
224
- # update local order
225
- @order.flow_data.merge!('digest' => @digest, 'order' => @response.to_hash)
226
- end
131
+ def write_response_to_order
132
+ return @order.flow_data.delete('order') if !@response || error?
227
133
 
228
- @order.update_column(:meta, @order.meta.to_json)
134
+ # update local order
135
+ @order.total = @response[:total]&.[](:amount)
136
+ @order.flow_data.merge!('order' => @response)
229
137
  end
230
138
  end
231
139
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class OrderUpdater
5
+ def initialize(order:)
6
+ unless order&.zone&.flow_io_active_or_archiving_experience?
7
+ raise(ArgumentError, 'Experience not defined or not active')
8
+ end
9
+
10
+ @experience = order.flow_io_experience_key
11
+ @order = order
12
+ @client = FlowcommerceSpree.client
13
+ end
14
+
15
+ def upsert_data(flow_io_order = nil)
16
+ return if @order.state == 'complete'
17
+
18
+ flow_io_order ||= @client.orders.get_by_number(FlowcommerceSpree::ORGANIZATION, @order.number).to_hash
19
+
20
+ @order.flow_data['order'] = flow_io_order
21
+ return if @order.flow_data.dig('order', 'submitted_at').blank?
22
+
23
+ attrs_to_update = { meta: @order.meta.to_json, email: @order.flow_customer_email, payment_state: 'pending' }
24
+ attrs_to_update.merge!(@order.prepare_flow_addresses)
25
+ @order.update_columns(attrs_to_update)
26
+ @order.state = 'delivery'
27
+ @order.save!
28
+ @order.create_proposed_shipments
29
+ @order.shipment.update_amounts
30
+ @order.line_items.each(&:store_ets)
31
+ @order.charge_taxes
32
+
33
+ @order.state = 'payment'
34
+ @order.save!
35
+ end
36
+
37
+ def finalize_order
38
+ @order.reload
39
+ @order.finalize!
40
+ @order.update_totals
41
+ @order.save
42
+ @order.after_completed_order
43
+ end
44
+
45
+ def complete_checkout
46
+ upsert_data
47
+ map_payments_to_spree
48
+ finalize_order if @order.state == 'complete'
49
+ end
50
+
51
+ def map_payments_to_spree
52
+ @order.flow_io_payments&.each do |p|
53
+ payment =
54
+ @order.payments.find_or_initialize_by(response_code: p['reference'], payment_method_id: payment_method_id)
55
+ next unless payment.new_record?
56
+
57
+ payment.amount = p.dig('total', 'amount')
58
+ payment.pend
59
+
60
+ # For now this additional update is overwriting the generated identifier with flow.io payment identifier.
61
+ # TODO: Check and possibly refactor in Spree 3.0, where the `before_create :set_unique_identifier`
62
+ # has been removed.
63
+ payment.update_column(:identifier, p['id'])
64
+ end
65
+
66
+ return if @order.payments.sum(:amount) < @order.amount || @order.state == 'complete'
67
+
68
+ @order.state = 'confirm'
69
+ @order.save!
70
+ @order.state = 'complete'
71
+ @order.save!
72
+ end
73
+
74
+ def payment_method_id
75
+ @payment_method_id ||= Spree::PaymentMethod.find_by(active: true, type: 'Spree::Gateway::FlowIo').id
76
+ end
77
+ end
78
+ end
@@ -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