flowcommerce_spree 0.0.1 → 0.0.6
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.
- checksums.yaml +4 -4
- data/README.md +101 -24
- data/SPREE_FLOW.md +6 -28
- data/app/controllers/concerns/current_zone_loader_decorator.rb +40 -25
- data/app/controllers/flowcommerce_spree/inventory_controller.rb +23 -0
- data/app/controllers/flowcommerce_spree/orders_controller.rb +20 -0
- data/app/controllers/flowcommerce_spree/webhooks_controller.rb +23 -13
- data/app/controllers/users/sessions_controller_decorator.rb +28 -0
- data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +4 -9
- data/app/models/spree/address_decorator.rb +19 -0
- data/app/models/spree/app_configuration_decorator.rb +7 -0
- data/app/models/spree/calculator/flow_io.rb +61 -0
- data/app/models/spree/calculator/shipping/flow_io.rb +40 -0
- data/app/models/spree/flow_io_credit_card_decorator.rb +21 -0
- data/app/models/spree/flow_io_order_decorator.rb +163 -0
- data/app/models/spree/flow_io_product_decorator.rb +2 -2
- data/app/models/spree/flow_io_variant_decorator.rb +4 -2
- data/app/models/spree/gateway/flow_io.rb +153 -0
- data/app/models/spree/{credit_card_decorator.rb → payment_capture_event_decorator.rb} +1 -1
- data/app/models/spree/promotion_handler/coupon_decorator.rb +1 -1
- data/app/models/spree/zones/flow_io_product_zone_decorator.rb +8 -0
- data/app/models/tracking/setup_decorator.rb +40 -0
- data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
- data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
- data/app/serializers/api/v2/order_serializer_decorator.rb +20 -0
- data/app/services/flowcommerce_spree/import_experience_items.rb +1 -1
- data/app/services/flowcommerce_spree/order_sync.rb +81 -173
- data/app/services/flowcommerce_spree/order_updater.rb +78 -0
- data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
- data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
- data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
- data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
- data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
- data/app/views/spree/admin/payments/source_views/_flow_io_gateway.html.erb +21 -0
- data/config/rails_best_practices.yml +51 -0
- data/config/routes.rb +3 -1
- data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +6 -4
- data/lib/flow/simple_gateway.rb +0 -36
- data/lib/flowcommerce_spree.rb +17 -3
- data/lib/flowcommerce_spree/engine.rb +33 -3
- data/lib/flowcommerce_spree/experience_service.rb +1 -27
- data/lib/flowcommerce_spree/logging_http_client.rb +33 -15
- data/lib/flowcommerce_spree/session.rb +16 -29
- data/lib/flowcommerce_spree/test_support.rb +7 -0
- data/lib/flowcommerce_spree/version.rb +1 -1
- data/lib/tasks/flowcommerce_spree.rake +4 -1
- metadata +90 -22
- data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
- data/app/models/spree/gateway/spree_flow_gateway.rb +0 -116
- data/app/models/spree/line_item_decorator.rb +0 -15
- data/app/models/spree/order_decorator.rb +0 -179
- data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
- data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
- data/config/initializers/flowcommerce_spree.rb +0 -7
- data/lib/flow/error.rb +0 -73
- data/lib/flow/pay_pal.rb +0 -25
- data/lib/flowcommerce_spree/webhook_service.rb +0 -98
- data/lib/simple_csv_writer.rb +0 -44
@@ -5,7 +5,7 @@ module Spree
|
|
5
5
|
Coupon.class_eval do
|
6
6
|
def apply
|
7
7
|
if order.coupon_code.present?
|
8
|
-
if promotion&.actions
|
8
|
+
if promotion&.actions&.exists?
|
9
9
|
experience_key = order.flow_order&.dig('experience', 'key')
|
10
10
|
forbiden_keys = promotion.flow_data&.dig('filter', 'experience') || []
|
11
11
|
|
@@ -16,6 +16,10 @@ module Spree
|
|
16
16
|
flow_data&.[]('key')
|
17
17
|
end
|
18
18
|
|
19
|
+
def flow_io_experience_country
|
20
|
+
flow_data&.[]('country')
|
21
|
+
end
|
22
|
+
|
19
23
|
def flow_io_experience_currency
|
20
24
|
flow_data&.[]('currency')
|
21
25
|
end
|
@@ -24,6 +28,10 @@ module Spree
|
|
24
28
|
flow_data&.[]('key').present? && flow_data['status'] == 'active'
|
25
29
|
end
|
26
30
|
|
31
|
+
def flow_io_active_or_archiving_experience?
|
32
|
+
flow_data&.[]('key').present? && %w[active archiving].include?(flow_data['status'])
|
33
|
+
end
|
34
|
+
|
27
35
|
def update_on_flow; end
|
28
36
|
|
29
37
|
def remove_on_flow_io
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tracking
|
4
|
+
Setup.module_eval do
|
5
|
+
private
|
6
|
+
|
7
|
+
def setup_tracking
|
8
|
+
return if request.path.start_with?(ADMIN_PATH)
|
9
|
+
|
10
|
+
user_consents = UserConsent.new(cookies)
|
11
|
+
setup_visitor_cookie(user_consents)
|
12
|
+
store_order_flow_io_attributes(user_consents) if current_order&.zone&.flow_io_active_experience?
|
13
|
+
end
|
14
|
+
|
15
|
+
def store_order_flow_io_attributes(user_consents)
|
16
|
+
# Using `save!` and not `update_column` for callbacks to work and sync the order to flow.io
|
17
|
+
current_order.save!(validate: false) if order_user_consents_updated?(user_consents) || user_uuid_updated?
|
18
|
+
end
|
19
|
+
|
20
|
+
def order_user_consents_updated?(user_consents)
|
21
|
+
consents_changed = nil
|
22
|
+
user_consents.active_groups.each do |consent_group|
|
23
|
+
group_value = consent_group[1][:value]
|
24
|
+
gdpr_group_name = "gdpr_#{consent_group[1][:name]}"
|
25
|
+
next if current_order.flow_io_attributes[gdpr_group_name] == group_value
|
26
|
+
|
27
|
+
consents_changed ||= true
|
28
|
+
current_order.flow_io_attribute_add(gdpr_group_name, group_value)
|
29
|
+
end
|
30
|
+
|
31
|
+
consents_changed
|
32
|
+
end
|
33
|
+
|
34
|
+
def user_uuid_updated?
|
35
|
+
return if current_order.flow_io_attr_user_uuid.present?
|
36
|
+
|
37
|
+
current_order.add_user_uuid_to_flow_data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -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
|
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 :
|
8
|
+
attr_reader :order, :response
|
21
9
|
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
@
|
37
|
-
@
|
38
|
-
@
|
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
|
-
|
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
|
-
|
50
|
-
|
51
|
-
end
|
26
|
+
sync_body!
|
27
|
+
write_response_to_order
|
52
28
|
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
@
|
41
|
+
@opts = { experience: @experience, expand: ['experience'] }
|
42
|
+
@body = { items: @order.line_items.map { |line_item| add_item(line_item) } }
|
105
43
|
|
106
|
-
|
107
|
-
@opts[:experience] = @experience
|
108
|
-
@opts[:expand] = ['experience']
|
44
|
+
try_to_add_customer
|
109
45
|
|
110
|
-
|
46
|
+
return unless (flow_data = @order.flow_data['order'])
|
111
47
|
|
112
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
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
|
134
|
-
return unless @
|
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
|
-
|
160
|
-
|
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
|
-
|
163
|
-
|
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
|
-
@
|
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
|
-
|
168
|
-
|
94
|
+
add_customer_address(address) if address
|
95
|
+
end
|
169
96
|
|
170
|
-
|
171
|
-
|
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
|
-
|
174
|
-
|
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
|
-
|
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
|
185
|
-
|
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
|
-
@
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
212
|
-
|
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
|
-
|
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
|