flowcommerce_spree 0.0.7 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aa92dd937ce7ff2ac430c684e76eaf18f80dc165191b5452407b73a51f6cc59
4
- data.tar.gz: 83270c61b581abf426a1e19d65c63d6c43ea4c766ba3bf67c3fc334cb234e6f6
3
+ metadata.gz: d0e6c714f8986d91af70150c8365f3bde79a15b843151cc84bf5fabd83cf7e87
4
+ data.tar.gz: 4b0b172606e24bddd55d8254da339af2bf4751b441d918478310835102c9f282
5
5
  SHA512:
6
- metadata.gz: c45520286feb21871014953ccaa0dd8d18d27520db9b31f05ef062f2e45d3d9b7b4de98807f46d3b4e0ec6acf63ed98e472fe0d37d4290f1b9bcd36ec21363db
7
- data.tar.gz: 37f630c22db5e978c4325ee6a5029cfe5c45940d817b537fd80f3bb750e63fd50d235796d078b0a6ac028136ab3075703b78030edd3a392c35f87e8bd2327740
6
+ metadata.gz: 915818b7f7ffd75a3e2adf67d25403b5ea342b0095f2b051d4044ef91c942bee15e2e6b0a235a62f53b7e95af88f6b2626f0a3a58767214e413ad9fc3695a36a
7
+ data.tar.gz: '048827e500e921d90c3dba7169644a09702f89551be7083820b0d4c337a97a0f513609417dee1f9017f266a8f28bf846dcb5c124262dee7fed03defecbda97fd'
@@ -4,5 +4,6 @@ module FlowcommerceSpree
4
4
  class Settings < Spree::Preferences::Configuration
5
5
  preference :additional_attributes, :hash, default: {}
6
6
  preference :product_catalog_upload, :hash, default: {}
7
+ preference :notification_setting, :hash, default: {}
7
8
  end
8
9
  end
@@ -11,8 +11,8 @@ module Spree
11
11
  order = item.order
12
12
 
13
13
  if can_calculate_tax?(order)
14
- flow_response = get_flow_tax_data(order)
15
- tax_for_item(item, flow_response)
14
+ get_flow_tax_data(order)
15
+ tax_for_item(item)
16
16
  else
17
17
  prev_tax_amount(item)
18
18
  end
@@ -20,6 +20,13 @@ module Spree
20
20
  alias compute_shipment compute_shipment_or_line_item
21
21
  alias compute_line_item compute_shipment_or_line_item
22
22
 
23
+ def get_tax_rate(taxable)
24
+ order = taxable.class.to_s == 'Spree::Order' ? taxable : taxable.order
25
+ get_flow_tax_data(order) if order.flow_allocations.empty?
26
+ response = order.flow_tax_for_item(taxable.adjustable, 'vat_item_price', rate.included_in_price)
27
+ response.nil? ? 0 : response['rate']&.to_f
28
+ end
29
+
23
30
  private
24
31
 
25
32
  def prev_tax_amount(item)
@@ -39,21 +46,26 @@ module Spree
39
46
 
40
47
  def get_flow_tax_data(order)
41
48
  flow_io_tax_response = Rails.cache.fetch(order.flow_tax_cache_key, time_to_idle: 5.minutes) do
42
- FlowcommerceSpree.client.orders.get_allocations_by_number(FlowcommerceSpree::ORGANIZATION, order.number)
49
+ response = FlowcommerceSpree.client.orders
50
+ .get_allocations_by_number(FlowcommerceSpree::ORGANIZATION, order.number)
51
+ return nil unless response.present?
52
+
53
+ order.flow_order['allocations'] = response.to_hash
54
+ order.update_column(:meta, order.meta.to_json)
55
+ response
43
56
  end
44
57
  flow_io_tax_response
45
58
  end
46
59
 
47
- def tax_for_item(item, flow_response)
60
+ def tax_for_item(item)
61
+ order = item.order
48
62
  prev_tax_amount = prev_tax_amount(item)
49
- return prev_tax_amount if flow_response.nil?
50
-
51
- item_details = flow_response.details&.find do |el|
52
- item.is_a?(Spree::LineItem) ? el.number == item.variant.sku : el.key.value == 'shipping'
53
- end
54
- price_components = rate.included_in_price ? item_details.included : item_details.not_included
63
+ tax_data = order.flow_tax_for_item(item, 'vat_item_price', rate.included_in_price)
64
+ return prev_tax_amount if tax_data.blank?
55
65
 
56
- amount = price_components&.find { |el| el.key.value == 'vat_item_price' }&.total&.amount
66
+ subsidy_data = order.flow_tax_for_item(item, 'vat_subsidy', rate.included_in_price)
67
+ amount = tax_data.dig('total', 'amount')
68
+ amount += subsidy_data.dig('total', 'amount') if subsidy_data.present?
57
69
  amount.present? && amount > 0 ? amount : prev_tax_amount
58
70
  end
59
71
  end
@@ -158,6 +158,24 @@ module Spree
158
158
  address_attributes
159
159
  end
160
160
 
161
+ def flow_allocations
162
+ return @flow_allocations if @flow_allocations
163
+
164
+ @flow_allocations = flow_order&.[]('allocations')
165
+ end
166
+
167
+ def flow_tax_for_item(item, tax_key, included_in_price = true)
168
+ return {} if flow_allocations.blank?
169
+
170
+ item_details = flow_allocations['details']&.find do |el|
171
+ item.is_a?(Spree::LineItem) ? el['number'] == item.variant.sku : el['key'] == 'shipping'
172
+ end
173
+ return {} if item_details.blank?
174
+
175
+ price_components = included_in_price ? item_details['included'] : item_details['not_included']
176
+ price_components&.find { |el| el['key'] == tax_key }
177
+ end
178
+
161
179
  Spree::Order.include(self) if Spree::Order.included_modules.exclude?(self)
162
180
  end
163
181
  end
@@ -7,6 +7,7 @@ module Spree
7
7
  base.serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
8
8
 
9
9
  base.store_accessor :meta, :flow_data, :zone_ids
10
+ base.after_save :sync_variants_with_flow
10
11
  end
11
12
 
12
13
  def price_in_zone(currency, product_zone)
@@ -27,9 +28,9 @@ module Spree
27
28
  flow_data["#{flow_exp.key}.excluded"].to_i != 1
28
29
  end
29
30
 
30
- def price_range(product_zone)
31
+ def price_range(product_zone, currencies = [])
31
32
  prices = {}
32
- master_prices.each do |p|
33
+ master_prices_with_currencies(currencies).each do |p|
33
34
  currency = p.currency
34
35
  min = nil
35
36
  max = nil
@@ -39,15 +40,15 @@ module Spree
39
40
  price = v.price_in(currency)
40
41
  next if price.nil? || price.amount.nil?
41
42
 
42
- min = price if min.nil? || min.amount > price.amount
43
- max = price if max.nil? || max.amount < price.amount
43
+ min = [price, min].compact.min { |a, b| a.amount <=> b.amount }
44
+ max = [price, max].compact.max { |a, b| a.amount <=> b.amount }
44
45
  end
45
46
  else
46
47
  min = max = master.price_in(currency)
47
48
  end
48
49
 
49
- rmin = min&.amount&.to_s(:rounded, precision: 0) || 0
50
- rmax = max&.amount&.to_s(:rounded, precision: 0) || 0
50
+ rmin = round_with_precision(min, 0)
51
+ rmax = round_with_precision(max, 0)
51
52
 
52
53
  prices[currency] = { min: rmin, max: rmax }
53
54
  end
@@ -55,6 +56,10 @@ module Spree
55
56
  add_flow_price_range(prices, product_zone)
56
57
  end
57
58
 
59
+ def round_with_precision(number, precision)
60
+ number&.amount&.to_s(:rounded, precision: precision) || 0
61
+ end
62
+
58
63
  def add_flow_price_range(prices, product_zone)
59
64
  flow_experience_key = product_zone&.flow_data&.[]('key')
60
65
  return prices if flow_experience_key.blank?
@@ -69,8 +74,8 @@ module Spree
69
74
  price = v.flow_local_price(flow_experience_key)
70
75
  next if price.amount.nil? || price.currency != currency
71
76
 
72
- min = price if min.nil? || min.amount > price.amount
73
- max = price if max.nil? || max.amount < price.amount
77
+ min = [price, min].compact.min { |a, b| a.amount <=> b.amount }
78
+ max = [price, max].compact.max { |a, b| a.amount <=> b.amount }
74
79
  end
75
80
  end
76
81
 
@@ -79,13 +84,17 @@ module Spree
79
84
  max ||= master_price
80
85
  end
81
86
 
82
- rmin = min&.amount&.to_s(:rounded, precision: 0) || 0
83
- rmax = max&.amount&.to_s(:rounded, precision: 0) || 0
87
+ rmin = round_with_precision(min, 0)
88
+ rmax = round_with_precision(max, 0)
84
89
 
85
90
  prices[currency] = { min: rmin, max: rmax }
86
91
  prices
87
92
  end
88
93
 
94
+ def sync_variants_with_flow
95
+ variants_including_master.each(&:sync_product_to_flow)
96
+ end
97
+
89
98
  Spree::Product.prepend(self) if Spree::Product.included_modules.exclude?(self)
90
99
  end
91
100
  end
@@ -49,17 +49,23 @@ module Spree
49
49
  product.update_columns(meta: product.meta.to_json)
50
50
  end
51
51
 
52
+ def sync_flow_info?
53
+ if FlowcommerceSpree::API_KEY.blank? || FlowcommerceSpree::API_KEY == 'test_key'
54
+ return { error: 'Api Keys not configured' }
55
+ end
56
+ return { error: 'Price is 0' } if price == 0
57
+ return { error: 'Country of Origin is empty.' } unless country_of_origin
58
+ end
59
+
52
60
  # upload product variant to Flow's Product Catalog
53
61
  def sync_product_to_flow
54
- # initial Spree seed will fail, so skip unless we have Flow data field
55
- return unless respond_to?(:flow_data)
56
-
57
- return if FlowcommerceSpree::API_KEY.blank? || FlowcommerceSpree::API_KEY == 'test_key'
62
+ error = sync_flow_info?
63
+ return error if error.present?
58
64
 
59
- return { error: 'Price is 0' } if price == 0
60
-
61
- return unless country_of_origin
65
+ update_flow_data
66
+ end
62
67
 
68
+ def update_flow_data
63
69
  additional_attrs = {}
64
70
  attr_name = nil
65
71
  export_required = false
@@ -84,7 +90,7 @@ module Spree
84
90
  flow_item_sh1 = Digest::SHA1.hexdigest(flow_item.to_json)
85
91
 
86
92
  # skip if sync not needed
87
- return nil if flow_data&.[](:last_sync_sh1) == flow_item_sh1
93
+ return { error: 'Synchronization not needed' } if flow_data&.[](:last_sync_sh1) == flow_item_sh1
88
94
 
89
95
  response = FlowcommerceSpree.client.items.put_by_number(FlowcommerceSpree::ORGANIZATION, sku, flow_item)
90
96
  self.flow_data ||= {}
@@ -93,6 +99,8 @@ module Spree
93
99
  # after successful put, write cache
94
100
  update_column(:meta, meta.to_json)
95
101
 
102
+ FlowcommerceSpree::ImportItemWorker.perform_async(sku)
103
+
96
104
  response
97
105
  rescue Net::OpenTimeout => e
98
106
  { error: e.message }
@@ -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
@@ -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
@@ -124,7 +124,7 @@ module FlowcommerceSpree
124
124
  { center: FLOW_CENTER,
125
125
  number: variant.sku,
126
126
  quantity: line_item.quantity,
127
- price: { amount: price_root['amount'] || variant.cost_price,
127
+ price: { amount: price_root['amount'] || variant.price,
128
128
  currency: price_root['currency'] || variant.cost_currency } }
129
129
  end
130
130
 
@@ -22,16 +22,25 @@ module FlowcommerceSpree
22
22
  errors << { message: 'Order number param missing' } && (return self) unless order_number
23
23
 
24
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
25
+ if order.payments.any?
26
+ store_payment_capture(order, capture)
27
+ else
28
+ FlowcommerceSpree::UpdatePaymentCaptureWorker.perform_in(1.minute, order.number, capture)
29
+ order
30
+ end
29
31
  else
30
32
  errors << { message: "Order #{order_number} not found" }
31
33
  self
32
34
  end
33
35
  end
34
36
 
37
+ def store_payment_capture(order, capture)
38
+ upsert_order_captures(order, capture)
39
+ payments = order.flow_io_payments
40
+ map_payment_captures_to_spree(order, payments) if payments.present?
41
+ order
42
+ end
43
+
35
44
  private
36
45
 
37
46
  def upsert_order_captures(order, capture)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class FlowIoWorker
5
+ include Sidekiq::Worker
6
+
7
+ sidekiq_retries_exhausted do |message, exception|
8
+ Rails.logger.warn("[!] #{self.class} max attempts reached: #{message} - #{exception}")
9
+ notification_setting = FlowcommerceSpree::Config.notification_setting
10
+ return unless notification_setting[:slack].present?
11
+
12
+ slack_message = "[#{Rails.env}] #{message}"
13
+ Slack_client.chat_postMessage(channel: notification_setting[:slack][:channel], text: slack_message)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class ImportItemWorker < FlowIoWorker
5
+ sidekiq_options retry: 3, queue: :flow_io
6
+
7
+ def perform(variant_sku)
8
+ variant = Spree::Variant.find_by sku: variant_sku
9
+ return unless variant
10
+
11
+ FlowcommerceSpree::ImportItem.run(variant)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ class UpdatePaymentCaptureWorker < FlowIoWorker
5
+ sidekiq_options retry: 3, queue: :flow_io
6
+
7
+ def perform(order_number, capture = {})
8
+ order = Spree::Order.find_by number: order_number
9
+ raise 'Order has no payments' if order.payments.empty?
10
+
11
+ FlowcommerceSpree::Webhooks::CaptureUpsertedV2.new({ capture: capture }.as_json)
12
+ .store_payment_capture(order, capture)
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowcommerceSpree
4
- VERSION = '0.0.7'
4
+ VERSION = '0.0.11'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flowcommerce_spree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aurel Branzeanu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-04-16 00:00:00.000000000 Z
12
+ date: 2021-08-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: active_model_serializers
@@ -230,6 +230,7 @@ files:
230
230
  - app/serializers/api/v2/order_serializer_decorator.rb
231
231
  - app/services/flowcommerce_spree/import_experience_items.rb
232
232
  - app/services/flowcommerce_spree/import_experiences.rb
233
+ - app/services/flowcommerce_spree/import_item.rb
233
234
  - app/services/flowcommerce_spree/order_sync.rb
234
235
  - app/services/flowcommerce_spree/order_updater.rb
235
236
  - app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb
@@ -243,6 +244,9 @@ files:
243
244
  - app/views/spree/admin/promotions/edit.html.erb
244
245
  - app/views/spree/admin/shared/_order_summary.html.erb
245
246
  - app/views/spree/admin/shared/_order_summary_flow.html.erb
247
+ - app/workers/flowcommerce_spree/flow_io_worker.rb
248
+ - app/workers/flowcommerce_spree/import_item_worker.rb
249
+ - app/workers/flowcommerce_spree/update_payment_capture_worker.rb
246
250
  - config/rails_best_practices.yml
247
251
  - config/routes.rb
248
252
  - db/migrate/20201021160159_add_type_and_meta_to_spree_zone.rb