flowcommerce_spree 0.0.4 → 0.0.5

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -6
  3. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +16 -18
  4. data/app/models/spree/app_configuration_decorator.rb +7 -0
  5. data/app/models/spree/calculator/shipping/flow_io.rb +5 -2
  6. data/app/models/spree/flow_io_order_decorator.rb +11 -58
  7. data/app/models/spree/flow_io_variant_decorator.rb +2 -0
  8. data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
  9. data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
  10. data/app/services/flowcommerce_spree/order_sync.rb +38 -81
  11. data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
  12. data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
  13. data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
  14. data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
  15. data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
  16. data/config/routes.rb +1 -1
  17. data/lib/flowcommerce_spree.rb +3 -1
  18. data/lib/flowcommerce_spree/engine.rb +5 -0
  19. data/lib/flowcommerce_spree/experience_service.rb +1 -27
  20. data/lib/flowcommerce_spree/version.rb +1 -1
  21. data/lib/tasks/flowcommerce_spree.rake +4 -1
  22. metadata +74 -17
  23. data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
  24. data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
  25. data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
  26. data/lib/flow/error.rb +0 -73
  27. data/lib/flow/pay_pal.rb +0 -25
  28. data/lib/flowcommerce_spree/webhook_service.rb +0 -154
  29. data/lib/simple_csv_writer.rb +0 -44
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- OrderMailer.class_eval do
5
- # default from: ApplicationMailer::DEFAULT_FROM
6
-
7
- def refund_complete_email(web_hook_event)
8
- auth_id = web_hook_event.dig('refund', 'authorization', 'key')
9
-
10
- raise Flow::Error, 'authorization key not found in WebHookEvent [refund_capture_upserted_v2]' unless auth_id
11
-
12
- authorization = FlowcommerceSpree.client.authorizations.get_by_key FlowcommerceSpree::ORGANIZATION, auth_id
13
-
14
- refund_requested = web_hook_event['refund']['requested']
15
- @mail_to = authorization.customer.email
16
- @full_name = "#{authorization.customer.name.first} #{authorization.customer.name.last}"
17
- @amount = "#{refund_requested['amount']} #{refund_requested['currency']}"
18
- @number = authorization.order.number
19
- @order = Spree::Order.find_by number: @number
20
-
21
- mail(to: @mail_to, subject: "We refunded your order for ammount #{@amount}")
22
- end
23
- end
24
- 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') %>
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,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FlowcommerceSpree
4
- # responds to webhook events from flow.io
5
- class WebhookService
6
- attr_accessor :errors
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
- hook_method = @data['discriminator']
21
- # If hook processing method registered an error, a self.object of WebhookService with this error will be
22
- # returned, else an ActiveRecord object will be returned
23
- return __send__(hook_method) if respond_to?(hook_method, true)
24
-
25
- errors << { message: "No hook for #{hook_method}" }
26
- self
27
- end
28
-
29
- private
30
-
31
- def capture_upserted_v2
32
- errors << { message: 'Capture param missing' } && (return self) unless (capture = @data['capture']&.to_hash)
33
-
34
- order_number = capture.dig('authorization', 'order', 'number')
35
- if (order = Spree::Order.find_by(number: order_number))
36
- order.flow_data['captures'] ||= []
37
- order_captures = order.flow_data['captures']
38
- order_captures.delete_if { |c| c['id'] == capture['id'] }
39
- order_captures << capture
40
- order.update_column(:meta, order.meta.to_json)
41
- map_payment_captures_to_spree(order) if order.flow_io_payments.present?
42
- order
43
- else
44
- errors << { message: "Order #{order_number} not found" }
45
- self
46
- end
47
- end
48
-
49
- def card_authorization_upserted_v2
50
- card_auth = @data['authorization']&.to_hash
51
- errors << { message: 'Authorization param missing' } && (return self) unless card_auth
52
-
53
- errors << { message: 'Card param missing' } && (return self) unless (flow_io_card = card_auth.delete('card'))
54
-
55
- if (order_number = card_auth.dig('order', 'number'))
56
- if (order = Spree::Order.find_by(number: order_number))
57
- flow_io_card_expiration = flow_io_card.delete('expiration')
58
-
59
- card = Spree::CreditCard.find_or_initialize_by(month: flow_io_card_expiration['month'].to_s,
60
- year: flow_io_card_expiration['year'].to_s,
61
- cc_type: flow_io_card.delete('type'),
62
- last_digits: flow_io_card.delete('last4'),
63
- name: flow_io_card.delete('name'),
64
- user_id: order.user&.id)
65
- card.flow_data ||= {}
66
- card.flow_data.merge!(flow_io_card.except('discriminator')) if card.new_record?
67
- card_auth['method'].delete('images')
68
- card.push_authorization(card_auth.except('discriminator'))
69
- if card.new_record?
70
- card.imported = true
71
- card.save!
72
- else
73
- card.update_column(:meta, card.meta.to_json)
74
- end
75
-
76
- order.payments.where(response_code: card_auth['id'])
77
- .update_all(source_id: card.id, source_type: 'Spree::CreditCard')
78
-
79
- return card
80
- else
81
- errors << { message: "Order #{order_number} not found" }
82
- end
83
- else
84
- errors << { message: 'Order number param missing' }
85
- end
86
-
87
- self
88
- end
89
-
90
- def experience_upserted_v2
91
- experience = @data['experience']
92
- Spree::Zones::Product.find_or_initialize_by(name: experience['key'].titleize).store_flow_io_data(experience)
93
- end
94
-
95
- def fraud_status_changed
96
- order_number = @data.dig('order', 'number')
97
- errors << { message: 'Order number param missing' } && (return self) unless order_number
98
-
99
- order = Spree::Order.find_by(number: order_number)
100
- errors << { message: "Order #{order_number} not found" } && (return self) unless order
101
-
102
- if @data['status'] == 'declined'
103
- order.update_columns(fraudulent: true)
104
- order.cancel!
105
- end
106
-
107
- order
108
- end
109
-
110
- def local_item_upserted
111
- errors << { message: 'Local item param missing' } && (return self) unless (local_item = @data['local_item'])
112
-
113
- errors << { message: 'SKU param missing' } && (return self) unless (flow_sku = local_item.dig('item', 'number'))
114
-
115
- if (variant = Spree::Variant.find_by(sku: flow_sku))
116
- variant.add_flow_io_experience_data(
117
- local_item.dig('experience', 'key'),
118
- 'prices' => [local_item.dig('pricing', 'price')], 'status' => local_item['status']
119
- )
120
-
121
- variant.update_column(:meta, variant.meta.to_json)
122
- return variant
123
- else
124
- errors << { message: "Variant with sku [#{flow_sku}] not found!" }
125
- end
126
-
127
- self
128
- end
129
-
130
- def map_payment_captures_to_spree(order)
131
- payments = order.flow_data&.dig('order', 'payments')
132
- order.flow_data['captures']&.each do |c|
133
- next unless c['status'] == 'succeeded'
134
-
135
- auth = c.dig('authorization', 'id')
136
- next unless payments&.find { |p| p['reference'] == auth }
137
-
138
- next unless (payment = Spree::Payment.find_by(response_code: auth))
139
-
140
- next if Spree::PaymentCaptureEvent.where("meta -> 'flow_data' ->> 'id' = ?", c['id']).exists?
141
-
142
- payment.capture_events.create!(amount: c['amount'], meta: { 'flow_data' => { 'id' => c['id'] } })
143
- return if payment.completed? || payment.capture_events.sum(:amount) < payment.amount
144
-
145
- payment.complete
146
- end
147
-
148
- return if order.completed?
149
- return unless order.flow_io_captures_sum >= order.flow_io_total_amount && order.flow_io_balance_amount <= 0
150
-
151
- FlowcommerceSpree::OrderUpdater.new(order: order).finalize_order
152
- end
153
- end
154
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # simple class to build scv files
4
-
5
- # csv = CsvWriter.new
6
- # csv.add a: 1, b: 'a', c: '"a'
7
- # csv.add a: ',', b: 'foo, bar'
8
- # csv.to_s
9
-
10
- class SimpleCsvWriter
11
- def initialize(delimiter: nil)
12
- @data = []
13
- @delimiter = delimiter || "\t"
14
- end
15
-
16
- # add hash or list
17
- def add(data)
18
- list = if data.class == Hash
19
- @keys ||= data.keys
20
- @keys.map { |key| data[key] }
21
- else
22
- data
23
- end
24
-
25
- @data.push list.map { |el| fmt(el) }.join(@delimiter)
26
- end
27
-
28
- def to_s
29
- if @keys
30
- @keys.map(&:to_s).join(@delimiter) + "\n" +
31
- @data.join($RS)
32
- else
33
- @data.join($RS)
34
- end
35
- end
36
-
37
- private
38
-
39
- def fmt(item)
40
- item = item.to_s.gsub($RS, '\\n').gsub('"', '""')
41
-
42
- item.include?(@delimiter) || item.include?('\\') ? "\"#{item}\"" : item
43
- end
44
- end