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
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Flow (2017)
4
- # Enable this modifications if you want to display flow localized line item
5
- # Example: https://i.imgur.com/7v2ix2G.png
6
- module Spree
7
- LineItem.class_eval do
8
- # admin show line item price
9
- def single_money
10
- price = display_price.to_s
11
- price += " (#{order.flow_line_item_price(self)})" if order.flow_order
12
- price
13
- end
14
- end
15
- end
@@ -1,179 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # `:display_total` modifications to display total prices beside Spree default. Example: https://i.imgur.com/7v2ix2G.png
4
- module Spree # rubocop:disable Metrics/ModuleLength
5
- # Added flow specific methods to Spree::Order
6
- Order.class_eval do
7
- serialize :meta, ActiveRecord::Coders::JSON.new(symbolize_keys: true)
8
-
9
- store_accessor :meta, :flow_data
10
-
11
- before_save :sync_to_flow_io
12
- after_touch :sync_to_flow_io
13
-
14
- def sync_to_flow_io
15
- return unless zone&.flow_io_active_experience? && state == 'cart' && line_items.size > 0
16
-
17
- flow_io_order = FlowcommerceSpree::OrderSync.new(order: self)
18
- flow_io_order.build_flow_request
19
- flow_io_order.synchronize! if flow_data['digest'] != flow_io_order.digest
20
- end
21
-
22
- def display_total
23
- price = FlowcommerceSpree::Api.format_default_price total
24
- price += " (#{flow_total})" if flow_order
25
- price.html_safe
26
- end
27
-
28
- def flow_order
29
- return unless flow_data&.[]('order')
30
-
31
- Hashie::Mash.new flow_data['order']
32
- end
33
-
34
- # accepts line item, usually called from views
35
- def flow_line_item_price(line_item, total = false)
36
- result = if flow_order
37
- id = line_item.variant.sku
38
-
39
- lines = flow_order.lines || []
40
- item = lines.find { |el| el['item_number'] == id }
41
-
42
- return 'n/a' unless item
43
-
44
- total ? item['total']['label'] : item['price']['label']
45
- else
46
- FlowcommerceSpree::Api.format_default_price(line_item.price * (total ? line_item.quantity : 1))
47
- end
48
-
49
- # add line item promo
50
- # promo_total, adjustment_total
51
- result += " (#{FlowcommerceSpree::Api.format_default_price(line_item.promo_total)})" if line_item.promo_total > 0
52
-
53
- result
54
- end
55
-
56
- # prepares array of prices that can be easily renderd in templates
57
- def flow_cart_breakdown
58
- prices = []
59
-
60
- price_model = Struct.new(:name, :label)
61
-
62
- if flow_order
63
- # duty, vat, ...
64
- unless flow_order.prices
65
- message = Flow::Error.format_order_message flow_order
66
- raise Flow::Error, message
67
- end
68
-
69
- flow_order.prices.each do |price|
70
- prices.push price_model.new(price['name'], price['label'])
71
- end
72
- else
73
- price_elements =
74
- %i[item_total adjustment_total included_tax_total additional_tax_total tax_total shipment_total promo_total]
75
- price_elements.each do |el|
76
- price = send(el)
77
- if price > 0
78
- label = FlowcommerceSpree::Api.format_default_price price
79
- prices.push price_model.new(el.to_s.humanize.capitalize, label)
80
- end
81
- end
82
-
83
- # discount is applied and we allways show it in default currency
84
- if adjustment_total != 0
85
- formated_discounted_price = FlowcommerceSpree::Api.format_default_price adjustment_total
86
- prices.push price_model.new('Discount', formated_discounted_price)
87
- end
88
- end
89
-
90
- # total
91
- prices.push price_model.new(Spree.t(:total), flow_total)
92
-
93
- prices
94
- end
95
-
96
- # shows localized total, if possible. if not, fall back to Spree default
97
- def flow_total
98
- # r flow_order.total.label
99
- price = flow_order&.total&.label
100
- price || FlowcommerceSpree::Api.format_default_price(total)
101
- end
102
-
103
- def flow_experience
104
- model = Struct.new(:key)
105
- model.new flow_order.experience.key
106
- rescue StandardError => _e
107
- model.new ENV.fetch('FLOW_BASE_COUNTRY')
108
- end
109
-
110
- def flow_io_experience_key
111
- flow_data&.[]('exp')
112
- end
113
-
114
- def flow_io_experience_from_zone
115
- self.flow_data = (flow_data || {}).merge!('exp' => zone.flow_io_experience)
116
- end
117
-
118
- def flow_io_order_id
119
- flow_data&.dig('order', 'id')
120
- end
121
-
122
- def flow_io_attributes
123
- flow_data&.dig('order', 'attributes') || {}
124
- end
125
-
126
- def add_user_consent_to_flow_data(consent, value)
127
- self.flow_data['order'] ||= {}
128
- self.flow_data['order']['attributes'] ||= {}
129
- self.flow_data['order']['attributes'][consent] = value
130
- end
131
-
132
- def add_user_uuid_to_flow_data
133
- self.flow_data['order'] ||= {}
134
- self.flow_data['order']['attributes'] ||= {}
135
- self.flow_data['order']['attributes']['user_uuid'] = user&.uuid
136
- end
137
-
138
- def flow_io_user_uuid
139
- flow_data&.dig('order', 'attributes', 'user_uuid')
140
- end
141
-
142
- def checkout_url
143
- "https://checkout.flow.io/#{FlowcommerceSpree::ORGANIZATION}/checkout/#{number}/" \
144
- "contact-info?flow_session_id=#{flow_data['session_id']}"
145
- end
146
-
147
- # clear invalid zero amount payments. Solidus bug?
148
- def clear_zero_amount_payments!
149
- # class attribute that can be set to true
150
- return unless Flow::Order.clear_zero_amount_payments
151
-
152
- payments.where(amount: 0, state: %w[invalid processing pending]).map(&:destroy)
153
- end
154
-
155
- def flow_order_authorized?
156
- flow_data&.[]('authorization') ? true : false
157
- end
158
-
159
- def flow_order_captured?
160
- flow_data['capture'] ? true : false
161
- end
162
-
163
- # completes order and sets all states to finalized and complete
164
- # used when we have confirmed capture from Flow API or PayPal
165
- def flow_finalize!
166
- finalize! unless state == 'complete'
167
- update_column :payment_state, 'paid' if payment_state != 'paid'
168
- update_column :state, 'complete' if state != 'complete'
169
- end
170
-
171
- def flow_payment_method
172
- if flow_data['payment_type'] == 'paypal'
173
- 'paypal'
174
- else
175
- 'cc' # creait card is default
176
- end
177
- end
178
- end
179
- end
@@ -1,86 +0,0 @@
1
- <%
2
- # render text email in console. parts[1] for html body
3
- # puts Spree::OrderMailer.confirm_email(Spree::Order.last).body.parts[0].body
4
-
5
- @prices = @order.flow_cart_breakdown
6
- @total_price = @prices.pop
7
- %>
8
-
9
- <style>
10
- table.order td { padding: 4px; border-top: 1px solid #bbb; }
11
- </style>
12
-
13
- <h6>Dear <%= @order.bill_address.firstname %></h6>
14
-
15
- <br>
16
- <br>
17
-
18
- <p><%= Spree.t('order_mailer.confirm_email.instructions') %></p>
19
- <p><%= Spree.t('order_mailer.confirm_email.order_summary') %></p>
20
-
21
- <table class="order">
22
- <tr>
23
- <th>Product</th>
24
- <th width="100" align="right">Price</th>
25
- <th width="80" align="center">Quantity</th>
26
- <th width="100" align="right">Total</th>
27
- </tr>
28
- <% @order.line_items.each do |line_item| %>
29
- <tr>
30
- <td><%= line_item.variant.product.name %></td>
31
- <td align="right"><%= @order.flow_line_item_price(line_item) %></td>
32
- <td align="center"><%= line_item.quantity %></td>
33
- <td align="right"><%= @order.flow_line_item_price(line_item, :with_quantity) %></td>
34
- </tr>
35
- <% end %>
36
- </table>
37
-
38
- <br>
39
-
40
- <p><b>Total</b></p>
41
-
42
- <table class="order">
43
-
44
- <% @prices.each do |price| %>
45
- <tr><td width="120"><%= price.name.capitalize %></td><td align="right"><%= price.label %></td></tr>
46
- <% end %>
47
-
48
- <tr>
49
- <td><%= Spree.t(:total) %></td>
50
- <td align="right"><b><%= @total_price.label %></b></td>
51
- </tr>
52
- <tr>
53
- <td>Payment method</td>
54
- <td align="right"><%= @order.flow_payment_method == 'paypal' ? 'PayPal' : 'Credit Card' %></td>
55
- </tr>
56
- </table>
57
-
58
- <br>
59
-
60
- <% ['ship', 'bill'].each do |name|
61
- address = @order.send('%s_address' % name)
62
- %>
63
- <p><b><%= name.capitalize %>ing address</b></p>
64
-
65
- <table class="order">
66
- <tr>
67
- <td>Full name</td>
68
- <td><%= address.firstname %> <%= address.lastname %></td>
69
- </tr>
70
- <tr>
71
- <td>Address</td>
72
- <td><%= address.address1 %></td>
73
- </tr>
74
- <tr>
75
- <td>City</td>
76
- <td><%= address.city %></td>
77
- </tr>
78
- <tr>
79
- <td>Country</td>
80
- <td><%= address.country.name rescue '-' %>, <%= address.state.name rescue '-' %></td>
81
- </tr>
82
- </table>
83
- <br />
84
- <% end %>
85
-
86
- <p><%= Spree.t('order_mailer.confirm_email.thanks') %></p>
@@ -1,38 +0,0 @@
1
- <%= Spree.t('order_mailer.confirm_email.dear_customer') %>
2
-
3
- <%= Spree.t('order_mailer.confirm_email.instructions') %>
4
-
5
- ============================================================
6
- <%= Spree.t('order_mailer.confirm_email.order_summary') %>
7
- ============================================================
8
- <% @order.line_items.each do |item| %>
9
- <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.single_money %> = <%= @order.flow_line_item_price(line_item, :with_quantity) %>
10
- <% end %>
11
- ============================================================
12
- <%= Spree.t('order_mailer.confirm_email.subtotal', :subtotal => @order.display_item_total) %>
13
- <% if @order.line_item_adjustments.exists? %>
14
- <% if @order.all_adjustments.promotion.eligible.exists? %>
15
- <% @order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %>
16
- <%= Spree.t(:promotion) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
17
- <% end %>
18
- <% end %>
19
- <% end %>
20
-
21
- <% @order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %>
22
- <%= Spree.t(:shipping) %>: <%= name %> <%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @order.currency) %>
23
- <% end %>
24
-
25
- <% if @order.all_adjustments.eligible.tax.exists? %>
26
- <% @order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %>
27
- <%= Spree.t(:tax) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
28
- <% end %>
29
- <% end %>
30
-
31
- <% @order.adjustments.eligible.each do |adjustment| %>
32
- <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %>
33
- <%= adjustment.label %> <%= adjustment.display_amount %>
34
- <% end %>
35
- ============================================================
36
- <%= Spree.t('order_mailer.confirm_email.total', :total => @order.display_total) %>
37
-
38
- <%= Spree.t('order_mailer.confirm_email.thanks') %>
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FlowcommerceSpree
4
- ORGANIZATION = ENV.fetch('FLOW_ORGANIZATION', 'flow.io')
5
- BASE_COUNTRY = ENV.fetch('FLOW_BASE_COUNTRY', 'USA')
6
- API_KEY = ENV.fetch('FLOW_TOKEN', 'test_key')
7
- end
data/lib/flow/error.rb DELETED
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Flow (2017)
4
- # api error logger and formater
5
-
6
- require 'digest/sha1'
7
-
8
- class Flow::Error < StandardError
9
- # logs error to file for easy discovery and fix
10
- def self.log(exception, request)
11
- history = exception.backtrace.reject { |el| el.index('/gems/') }.map { |el| el.sub(Rails.root.to_s, '') }.join($/)
12
-
13
- msg = "#{exception.class} in #{request.url}"
14
- data = [msg, exception.message, history].join("\n\n")
15
- key = Digest::SHA1.hexdigest(exception.backtrace.first.split(' ').first)
16
-
17
- folder = Rails.root.join('log/exceptions').to_s
18
- Dir.mkdir(folder) unless Dir.exist?(folder)
19
-
20
- folder += "/#{exception.class.to_s.tableize.gsub('/', '-')}"
21
- Dir.mkdir(folder) unless Dir.exist?(folder)
22
-
23
- "#{folder}/#{key}.txt".tap do |path|
24
- File.write(path, data)
25
- end
26
- end
27
-
28
- def self.format_message(exception)
29
- # format Flow errors in a special way
30
- # Io::Flow::V0::HttpClient::ServerError - 422 Unprocessable Entity:
31
- # {"code":"invalid_number","messages":["Card number is not valid"]}
32
- # hash['code'] = 'invalid_number'
33
- # hash['message'] = 'Card number is not valid'
34
- # hash['title'] = '422 Unprocessable Entity'
35
- # hash['klass'] = 'Io::Flow::V0::HttpClient::ServerError'
36
- if exception.class == Io::Flow::V0::HttpClient::ServerError
37
- parts = exception.message.split(': ', 2)
38
- hash = Oj.load(parts[1])
39
-
40
- hash[:message] = hash['messages'].join(', ')
41
- hash[:title] = parts[0]
42
- hash[:klass] = exception.class
43
- hash[:code] = hash['code']
44
- else
45
- msg = exception.message.is_a?(Array) ? exception.message.join(' - ') : exception.message
46
-
47
- hash = {}
48
- hash[:message] = msg
49
- hash[:title] = '-'
50
- hash[:klass] = exception.class
51
- hash[:code] = '-'
52
- end
53
-
54
- hash
55
- end
56
-
57
- def self.format_order_message(order)
58
- message = if order['messages']
59
- msg = order['messages'].join(', ')
60
- msg += " (#{Spree::Variant.where(id: order['numbers']).map(&:name).join(', ')})" if order['numbers']
61
- msg
62
- else
63
- 'Order not properly localized (sync issue)'
64
- end
65
-
66
- # sub_info = 'Flow.io'
67
- # sub_info += ' - %s' % flow_experience.key[0, 15] if flow_experience
68
-
69
- # '%s (%s)' % [message, sub_info]
70
-
71
- message
72
- end
73
- end
data/lib/flow/pay_pal.rb DELETED
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Flow.io (2017)
4
- # communicates with flow api to synchronize Spree order with PayPal
5
-
6
- module Flow::PayPal
7
- extend self
8
-
9
- def get_id(order)
10
- raise 'PayPal only supported while using flow' unless order.flow_order
11
-
12
- # get PayPal ID using Flow api
13
- body = {
14
- # discriminator: 'merchant_of_record_payment_form',
15
- method: 'paypal',
16
- order_number: order.number,
17
- amount: order.flow_order.total.amount,
18
- currency: order.flow_order.total.currency
19
- }
20
-
21
- # FlowcommerceSpree::Api.run :post, '/:organization/payments', {}, body
22
- form = ::Io::Flow::V0::Models::MerchantOfRecordPaymentForm.new body
23
- FlowcommerceSpree.client.payments.post FlowcommerceSpree::ORGANIZATION, form
24
- end
25
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FlowcommerceSpree
4
- # communicates with flow api, responds to webhook events
5
- class WebhookService
6
- attr_accessor :errors, :product, :variant
7
- alias full_messages errors
8
-
9
- def self.process(data, opts = {})
10
- new(data, opts).process
11
- end
12
-
13
- def initialize(data, opts = {})
14
- @data = data
15
- @opts = opts
16
- @errors = []
17
- end
18
-
19
- def process
20
- org = @data['organization']
21
- if org != ORGANIZATION
22
- errors << { message: "Organization name mismatch for #{org}" }
23
- else
24
- discriminator = @data['discriminator']
25
- hook_method = "hook_#{discriminator}"
26
- # If hook processing method registered an error, a self.object of WebhookService with this error will be
27
- # returned, else an ActiveRecord object will be returned
28
- return __send__(hook_method) if respond_to?(hook_method, true)
29
-
30
- errors << { message: "No hook for #{discriminator}" }
31
- end
32
-
33
- self
34
- end
35
-
36
- private
37
-
38
- def hook_experience_upserted_v2
39
- experience = @data['experience']
40
- Spree::Zones::Product.find_or_initialize_by(name: experience['key'].titleize).store_flow_io_data(experience)
41
- end
42
-
43
- def hook_local_item_upserted
44
- if (local_item = @data['local_item'])
45
- if (received_sku = local_item.dig('item', 'number'))
46
- if (@variant = Spree::Variant.find_by(sku: received_sku))
47
- @variant.add_flow_io_experience_data(
48
- local_item.dig('experience', 'key'),
49
- 'prices' => [local_item.dig('pricing', 'price')], 'status' => local_item['status']
50
- )
51
-
52
- @variant.update_column(:meta, @variant.meta.to_json)
53
- return @variant
54
- else
55
- errors << { message: "Variant with sku [#{received_sku}] not found!" }
56
- end
57
- else
58
- errors << { message: 'SKU param missing' }
59
- end
60
- else
61
- errors << { message: 'Local item param missing' }
62
- end
63
-
64
- self
65
- end
66
-
67
- def hook_order_upserted_v2
68
- errors << { message: 'Order param missing' } unless (received_order = @data['order'])
69
-
70
- if errors.none? && (order_number = received_order['number'])
71
- if (order = Spree::Order.find_by(number: order_number))
72
- order.flow_data['order'] = received_order.to_hash
73
- attrs_to_update = { meta: order.meta.to_json }
74
- if order.flow_data['order']['submitted_at'].present?
75
- attrs_to_update[:state] = 'complete'
76
- attrs_to_update[:completed_at] = Time.zone.now
77
- end
78
-
79
- order.update_columns(attrs_to_update)
80
- return order
81
- else
82
- errors << { message: "Order #{order_number} not found" }
83
- end
84
- else
85
- errors << { message: 'Order number param missing' }
86
- end
87
-
88
- self
89
- end
90
-
91
- # send en email when order is refunded
92
- def hook_refund_upserted_v2
93
- Spree::OrderMailer.refund_complete_email(@data).deliver
94
-
95
- 'Email delivered'
96
- end
97
- end
98
- end