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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class Calculator
5
+ module Shipping
6
+ class FlowIo < ShippingCalculator
7
+ preference :lower_boundary, :decimal, default: 100
8
+ preference :charge_default, :decimal, default: 15
9
+
10
+ def self.description
11
+ 'FlowIO Calculator'
12
+ end
13
+
14
+ def compute_package(package)
15
+ flow_order = flow_order(package)
16
+ return unless flow_order
17
+
18
+ flow_order['prices'].find { |x| x['key'] == 'shipping' }['amount'] || 0
19
+ end
20
+
21
+ def default_charge(_country)
22
+ preferred_charge_default
23
+ end
24
+
25
+ def threshold
26
+ preferred_lower_boundary
27
+ end
28
+
29
+ private
30
+
31
+ def flow_order(package)
32
+ return @flow_order if defined?(@flow_order)
33
+
34
+ @flow_order = package.order.flow_data&.[]('order')
35
+ @flow_order
36
+ end
37
+ end
38
+ end
39
+ end
40
+ 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
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # Added flow specific methods to Spree::Order
5
+ module FlowIoOrderDecorator
6
+ def self.included(base)
7
+ base.serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
8
+
9
+ base.store_accessor :meta, :flow_data
10
+ end
11
+
12
+ def flow_tax_cache_key
13
+ [number, 'flowcommerce', 'allocation', line_items.sum(:quantity)].join('-')
14
+ end
15
+
16
+ def display_total
17
+ return unless flow_data&.[]('order')
18
+
19
+ Spree::Money.new(flow_io_total_amount, currency: currency)
20
+ end
21
+
22
+ def flow_order
23
+ flow_data&.[]('order')
24
+ end
25
+
26
+ def flow_order_with_payments?
27
+ payment = payments.completed.first
28
+
29
+ payment&.payment_method&.type == 'Spree::Gateway::FlowIo'
30
+ end
31
+
32
+ # accepts line item, usually called from views
33
+ def flow_line_item_price(line_item, total = false)
34
+ result = if (order = flow_order)
35
+ item = order['lines']&.find { |el| el['item_number'] == line_item.variant.sku }
36
+
37
+ return 'n/a' unless item
38
+
39
+ total ? item['total']['label'] : item['price']['label']
40
+ else
41
+ FlowcommerceSpree::Api.format_default_price(line_item.price * (total ? line_item.quantity : 1))
42
+ end
43
+
44
+ # add line item promo
45
+ # promo_total, adjustment_total
46
+ result += " (#{FlowcommerceSpree::Api.format_default_price(line_item.promo_total)})" if line_item.promo_total > 0
47
+
48
+ result
49
+ end
50
+
51
+ # shows localized total, if possible. if not, fall back to Spree default
52
+ def flow_io_total_amount
53
+ flow_data&.dig('order', 'total', 'amount')&.to_d || 0
54
+ end
55
+
56
+ def flow_io_experience_key
57
+ flow_data&.[]('exp')
58
+ end
59
+
60
+ def flow_io_experience_from_zone
61
+ self.flow_data = (flow_data || {}).merge!('exp' => zone.flow_io_experience)
62
+ end
63
+
64
+ def flow_io_order_id
65
+ flow_data&.dig('order', 'id')
66
+ end
67
+
68
+ def flow_io_attributes
69
+ flow_data&.dig('order', 'attributes') || {}
70
+ end
71
+
72
+ def flow_io_attribute_add(attr_key, value)
73
+ self.flow_data['order'] ||= {}
74
+ self.flow_data['order']['attributes'] ||= {}
75
+ self.flow_data['order']['attributes'][attr_key] = value
76
+ end
77
+
78
+ def add_user_uuid_to_flow_data
79
+ self.flow_data['order'] ||= {}
80
+ self.flow_data['order']['attributes'] ||= {}
81
+ self.flow_data['order']['attributes']['user_uuid'] = user&.uuid || ''
82
+ end
83
+
84
+ def flow_io_attr_user_uuid
85
+ flow_data&.dig('order', 'attributes', 'user_uuid')
86
+ end
87
+
88
+ def flow_io_captures
89
+ flow_data&.[]('captures')
90
+ end
91
+
92
+ def flow_io_captures_sum
93
+ captures_sum = 0
94
+ flow_data&.[]('captures')&.each do |c|
95
+ next if c['status'] != 'succeeded'
96
+
97
+ amount = c['amount']
98
+ amount = amount.to_d if amount.is_a?(String)
99
+ captures_sum += amount
100
+ end
101
+ captures_sum.to_d
102
+ end
103
+
104
+ def flow_io_balance_amount
105
+ flow_data&.dig('order', 'balance', 'amount')&.to_d || 0
106
+ end
107
+
108
+ def flow_io_payments
109
+ flow_data.dig('order', 'payments')
110
+ end
111
+
112
+ def flow_customer_email
113
+ flow_data.dig('order', 'customer', 'email')
114
+ end
115
+
116
+ def flow_ship_address
117
+ flow_destination = flow_data.dig('order', 'destination')
118
+ return unless flow_destination.present?
119
+
120
+ flow_destination['first'] = flow_destination.dig('contact', 'name', 'first')
121
+ flow_destination['last'] = flow_destination.dig('contact', 'name', 'last')
122
+ flow_destination['phone'] = flow_destination.dig('contact', 'phone')
123
+
124
+ s_address = ship_address || build_ship_address
125
+ s_address.prepare_from_flow_attributes(flow_destination)
126
+ s_address
127
+ end
128
+
129
+ def flow_bill_address
130
+ flow_payment_address = flow_data.dig('order', 'payments')&.last&.[]('address')
131
+ return unless flow_payment_address
132
+
133
+ flow_payment_address['first'] = flow_payment_address.dig('name', 'first')
134
+ flow_payment_address['last'] = flow_payment_address.dig('name', 'last')
135
+ flow_payment_address['phone'] = ship_address['phone']
136
+
137
+ b_address = bill_address || build_bill_address
138
+ b_address.prepare_from_flow_attributes(flow_payment_address)
139
+ b_address
140
+ end
141
+
142
+ def prepare_flow_addresses
143
+ address_attributes = {}
144
+
145
+ s_address = flow_ship_address
146
+
147
+ if s_address&.changes&.any?
148
+ s_address.save!
149
+ address_attributes[:ship_address_id] = s_address.id unless ship_address_id
150
+ end
151
+
152
+ b_address = flow_bill_address
153
+ if b_address&.changes&.any?
154
+ b_address.save!
155
+ address_attributes[:bill_address_id] = b_address.id unless bill_address_id
156
+ end
157
+
158
+ address_attributes
159
+ end
160
+
161
+ Spree::Order.include(self) if Spree::Order.included_modules.exclude?(self)
162
+ end
163
+ end
@@ -22,8 +22,8 @@ module Spree
22
22
  raise ArgumentError, 'Value should be a hash' unless value.is_a?(Hash)
23
23
 
24
24
  self.flow_data = flow_data || {}
25
- self.flow_data['exp'] ||= {}
26
- self.flow_data['exp'][exp] = value
25
+ self.flow_data['exp'] ||= {} # rubocop:disable Style/RedundantSelf
26
+ self.flow_data['exp'][exp] = value # rubocop:disable Style/RedundantSelf
27
27
  end
28
28
 
29
29
  # clears flow_data from the records
@@ -58,6 +58,8 @@ module Spree
58
58
 
59
59
  return { error: 'Price is 0' } if price == 0
60
60
 
61
+ return unless country_of_origin
62
+
61
63
  additional_attrs = {}
62
64
  attr_name = nil
63
65
  export_required = false
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Flow.io (2017)
4
+ # adapter for Spree that talks to activemerchant_flow
5
+ module Spree
6
+ class Gateway
7
+ class FlowIo < Gateway
8
+ REFUND_SUCCESS = 'succeeded'
9
+
10
+ def provider_class
11
+ self.class
12
+ end
13
+
14
+ def actions
15
+ %w[capture authorize purchase refund void]
16
+ end
17
+
18
+ # if user wants to force auto capture
19
+ def auto_capture?
20
+ false
21
+ end
22
+
23
+ def payment_profiles_supported?
24
+ true
25
+ end
26
+
27
+ def method_type
28
+ 'flow_io_gateway'
29
+ end
30
+
31
+ def preferences
32
+ {}
33
+ end
34
+
35
+ def supports?(source)
36
+ # flow supports credit cards
37
+ source.class == Spree::CreditCard
38
+ end
39
+
40
+ def authorize(_amount, _payment_method, options = {})
41
+ order = load_order options
42
+ order.cc_authorization
43
+ end
44
+
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, {}, {})
49
+ end
50
+
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, {}, {})
56
+ end
57
+
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)
65
+ end
66
+
67
+ def create_profile(payment)
68
+ # payment.order.state
69
+ @credit_card = payment.source
70
+
71
+ profile_ensure_payment_method_is_present!
72
+ create_flow_cc_profile!
73
+ end
74
+
75
+ private
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
+
117
+ # hard inject Flow as payment method unless defined
118
+ def profile_ensure_payment_method_is_present!
119
+ return if @credit_card.payment_method_id
120
+
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
123
+ end
124
+
125
+ # create payment profile with Flow and tokenize Credit Card
126
+ def create_flow_cc_profile!
127
+ return if @credit_card.gateway_customer_profile_id
128
+ return unless @credit_card.verification_value
129
+
130
+ # build credit card hash
131
+ data = {}
132
+ data[:number] = @credit_card.number
133
+ data[:name] = @credit_card.name
134
+ data[:cvv] = @credit_card.verification_value
135
+ data[:expiration_year] = @credit_card.year.to_i
136
+ data[:expiration_month] = @credit_card.month.to_i
137
+
138
+ # tokenize with Flow
139
+ # rescue Io::Flow::V0::HttpClient::ServerError
140
+ card_form = ::Io::Flow::V0::Models::CardForm.new(data)
141
+ result = FlowcommerceSpree.client.cards.post(::FlowcommerceSpree::ORGANIZATION, card_form)
142
+
143
+ @credit_card.update_column :gateway_customer_profile_id, result.token
144
+ end
145
+
146
+ def load_order(options)
147
+ order_number = options[:order_id].split('-').first
148
+ spree_order = Spree::Order.find_by number: order_number
149
+ ::Flow::SimpleGateway.new spree_order
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spree
4
- CreditCard.class_eval do
4
+ PaymentCaptureEvent.class_eval do
5
5
  serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
6
6
 
7
7
  store_accessor :meta, :flow_data
@@ -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.exists?
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