flowcommerce_spree 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3810faadd82b0d21c0bacaeaa6be61bfe314ddffe4f32a6805c05554c592a745
4
- data.tar.gz: 6d042ed69b193082492ac992ea3d4a9bd962e493ae49026a43c297492013b4ea
3
+ metadata.gz: c3c70c6b82576ce2c4aa02c3b68a34c1c7b17360099ec14b59113d9fe59600d4
4
+ data.tar.gz: 6350b688c98c72b9c4a63ac4b91e412cf33683739e710c49ea85b3d4e3b6234e
5
5
  SHA512:
6
- metadata.gz: 6bd6d8e87e8f5d8c5d30d9f4366bc0e6d06dc95504d6f8cd3fc8916c752478123e80671ac5da4063a02c6051eb56d95f14ef97d782249a7fa9cb1df4e983829f
7
- data.tar.gz: 260d3b14add605bc114bd561a39f7376a88850d4aea95a98339db5461a9845463a87da1cd845cf68a6b8732a88d7f445a8961fb4f5a0847c282130efeb247083
6
+ metadata.gz: 651de4bcd8137242f0f6b50d21ccc2971f4b862d8e486f2087b3df19635f62c4ab7d2c00162d7801b27bd17fff8cf16c0df902e3cd3feb84f40513937cd483ba
7
+ data.tar.gz: c4ebc042ff007ccfc9fed580fd152f7be335eb3f4b899fc858511380a3bc6037bfb2d91ecfb28d9624731a433f49a25f8016a374de02602ad848ae88e27223cc
data/README.md CHANGED
@@ -20,8 +20,15 @@ All flowcommerce_spree code is located in the ./app and ./lib folders.
20
20
 
21
21
  - Run `bundle install`.
22
22
 
23
- - Define this additional ENV variables. You will find all of them, except FLOW_MOUNT_PATH in
24
- [Flow console](https://console.flow.io/org_account_name/organization/integrations):
23
+ - Define these additional ENV variables.
24
+ - You will find FLOW_TOKEN, FLOW_ORGANIZATION and FLOW_BASE_COUNTRY in [Flow
25
+ console](https://console.flow.io/org_account_name/organization/integrations)
26
+ - To enable HTTP Basic authentication for securing the FlowcommerceSpree::WebhooksController, prepend
27
+ username:password@ to the hostname in your webhook URL.
28
+ By doing so, the credentials needed for authentication will be sent in the HTTP header.
29
+ For example: https://username:password@www.mywebhookurl.com
30
+ On the main app's backend side, the `username` and `password` values should be defined in the
31
+ FLOW_IO_WEBHOOK_USER and FLOW_IO_WEBHOOK_PASSWORD environment variables
25
32
 
26
33
  ```
27
34
  FLOW_TOKEN='SUPERsecretTOKEN' # API_KEY
@@ -29,7 +36,10 @@ All flowcommerce_spree code is located in the ./app and ./lib folders.
29
36
  FLOW_BASE_COUNTRY='usa'
30
37
  # The path to which the FlowcommerceSpree engine will be mounted (default, if this variable is missing, will be the
31
38
  # '/flow' path)
32
- FLOW_MOUNT_PATH='/flow'
39
+ FLOW_MOUNT_PATH='/flow'
40
+ # The following variables should be set for securing the FlowcommerceSpree::WebhooksControler
41
+ FLOW_IO_WEBHOOK_USER
42
+ FLOW_IO_WEBHOOK_PASSWORD
33
43
  ```
34
44
 
35
45
  - To enable payments with the FlowCommerce engine, the payment method `flow.io` with `Spree::Gateway::FlowIo` should be
@@ -86,7 +96,9 @@ being used, depending on the level of modification.
86
96
 
87
97
  ### Spree::Gateway::FlowIo
88
98
 
89
- Adapter for Spree, that allows using [Flow.io](https://www.flow.io) as payment gateway. Flow is PCI compliant payment processor.
99
+ Adapter for Spree, that allows using [Flow.io](https://www.flow.io) as payment gateway.
100
+ Flow is PCI compliant payment processor.
101
+
90
102
 
91
103
  ## Gem Maintenance
92
104
 
@@ -129,11 +141,28 @@ by the following command:
129
141
  gem build flowcommerce_spree.gemspec
130
142
  ```
131
143
 
132
- Asuming the version was set to `0.0.1`, a `flowcommerce_spree-0.0.1.gem` will be generated at the root of the app
133
- (repo).
144
+ Assuming the version was set to `0.0.1`,
145
+ a `flowcommerce_spree-0.0.1.gem` binary file will be generated at the root of the app (repo).
146
+
147
+ - The binary file shouldn't be added into the `git` tree, it will be pushed into the RubyGems and to the GitHub releases
134
148
 
135
149
  ### Pushing a new gem release to RubyGems
136
150
 
137
151
  ```
138
152
  gem push flowcommerce_spree-0.0.1.gem # don't forget to specify the correct version number
139
153
  ```
154
+
155
+ ### Crafting the new release on GitHub
156
+
157
+ On the [Releases page](https://github.com/mejuri-inc/flowcommerce_spree/releases) push the `Draft a new release` button.
158
+
159
+ The new release editing page opens, on which the following actions could be taken:
160
+
161
+ - Choose the repo branch (default is `main`)
162
+ - Insert a tag version (usually, the tag should correspond to the gem's new version, v0.0.1, for example)
163
+ - the tag will be created by GitHub on the last commit into the chosen branch
164
+ - Fill the release Title and Description
165
+ - Attach the binary file with the generated gem version
166
+ - If the release is not yet ready for production, mark the `This is a pre-release` checkbox
167
+ - Press either the `Publish release`, or the `Save draft button` if you want to publish it later
168
+ - After publishing the release, the the binary gem file will be available on GitHub and could be removed locally
@@ -4,34 +4,32 @@ module FlowcommerceSpree
4
4
  class WebhooksController < ActionController::Base
5
5
  wrap_parameters false
6
6
  respond_to :json
7
+ http_basic_authenticate_with name: FLOW_IO_WEBHOOK_USER, password: FLOW_IO_WEBHOOK_PASSWORD
7
8
 
8
- # forward all incoming requests to Flow WebhookService object
9
+ # forward incoming requests to respective Flow Webhooks Service objects
9
10
  # /flow/event-target endpoint
10
- def handle_flow_web_hook_event
11
- result = check_organization
12
- if result.blank?
13
- webhook_result = WebhookService.process(params)
14
- result[:error] = webhook_result.full_messages.join("\n") if webhook_result.errors.any?
15
- end
11
+ def handle_flow_io_event
12
+ %i[event_id organization discriminator].each_with_object(params) { |key, obj| obj.require(key) }
13
+ return unless organization_valid?
14
+
15
+ webhook_result = "FlowcommerceSpree::Webhooks::#{params['discriminator'].classify}".constantize.process(params)
16
+ @result = {}
17
+ @result[:error] = webhook_result.full_messages.join("\n") if webhook_result.errors.any?
16
18
  rescue StandardError => e
17
- result = { error: e.class.to_s, message: e.message, backtrace: e.backtrace }
19
+ @result = { error: e.class.to_s, message: e.message, backtrace: e.backtrace }
18
20
  ensure
19
- response_status = if result[:error]
20
- logger.info(result)
21
- :unprocessable_entity
22
- else
23
- :ok
24
- end
25
- render json: result.except(:backtrace), status: response_status
21
+ logger.info(@result) if (error = @result[:error])
22
+ render json: @result.except(:backtrace), status: error ? :unprocessable_entity : :ok
26
23
  end
27
24
 
28
25
  private
29
26
 
30
- def check_organization
27
+ def organization_valid?
31
28
  org = params[:organization]
32
- return {} if org == FlowcommerceSpree::ORGANIZATION
29
+ return true if org == FlowcommerceSpree::ORGANIZATION
33
30
 
34
- { error: 'InvalidParam', message: "Organization '#{org}' is invalid!" }
31
+ @result = { error: 'InvalidParam', message: "Organization '#{org}' is invalid!" }
32
+ false
35
33
  end
36
34
  end
37
35
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ AppConfiguration.class_eval do
5
+ preference :debug_request_ip_address, :string
6
+ end
7
+ end
@@ -4,6 +4,9 @@ module Spree
4
4
  class Calculator
5
5
  module Shipping
6
6
  class FlowIo < ShippingCalculator
7
+ preference :lower_boundary, :decimal, default: 100
8
+ preference :charge_default, :decimal, default: 15
9
+
7
10
  def self.description
8
11
  'FlowIO Calculator'
9
12
  end
@@ -16,11 +19,11 @@ module Spree
16
19
  end
17
20
 
18
21
  def default_charge(_country)
19
- 0
22
+ preferred_charge_default
20
23
  end
21
24
 
22
25
  def threshold
23
- 0
26
+ preferred_lower_boundary
24
27
  end
25
28
 
26
29
  private
@@ -23,6 +23,12 @@ module Spree
23
23
  flow_data&.[]('order')
24
24
  end
25
25
 
26
+ def flow_order_with_payments?
27
+ payment = payments.completed.first
28
+
29
+ payment&.payment_method&.type == 'Spree::Gateway::FlowIo'
30
+ end
31
+
26
32
  # accepts line item, usually called from views
27
33
  def flow_line_item_price(line_item, total = false)
28
34
  result = if (order = flow_order)
@@ -42,56 +48,9 @@ module Spree
42
48
  result
43
49
  end
44
50
 
45
- # prepares array of prices that can be easily renderd in templates
46
- def flow_cart_breakdown
47
- prices = []
48
-
49
- price_model = Struct.new(:name, :label)
50
-
51
- if flow_order
52
- # duty, vat, ...
53
- unless flow_order.prices
54
- message = Flow::Error.format_order_message flow_order
55
- raise Flow::Error, message
56
- end
57
-
58
- flow_order.prices.each do |price|
59
- prices.push price_model.new(price['name'], price['label'])
60
- end
61
- else
62
- price_elements =
63
- %i[item_total adjustment_total included_tax_total additional_tax_total tax_total shipment_total promo_total]
64
- price_elements.each do |el|
65
- price = send(el)
66
- if price > 0
67
- label = FlowcommerceSpree::Api.format_default_price price
68
- prices.push price_model.new(el.to_s.humanize.capitalize, label)
69
- end
70
- end
71
-
72
- # discount is applied and we allways show it in default currency
73
- if adjustment_total != 0
74
- formated_discounted_price = FlowcommerceSpree::Api.format_default_price adjustment_total
75
- prices.push price_model.new('Discount', formated_discounted_price)
76
- end
77
- end
78
-
79
- # total
80
- prices.push price_model.new(Spree.t(:total), flow_total)
81
-
82
- prices
83
- end
84
-
85
51
  # shows localized total, if possible. if not, fall back to Spree default
86
52
  def flow_io_total_amount
87
- flow_data&.dig('order', 'total', 'amount')&.to_d
88
- end
89
-
90
- def flow_experience
91
- model = Struct.new(:key)
92
- model.new flow_order.experience.key
93
- rescue StandardError => _e
94
- model.new ENV.fetch('FLOW_BASE_COUNTRY')
53
+ flow_data&.dig('order', 'total', 'amount')&.to_d || 0
95
54
  end
96
55
 
97
56
  def flow_io_experience_key
@@ -135,27 +94,21 @@ module Spree
135
94
  flow_data&.[]('captures')&.each do |c|
136
95
  next if c['status'] != 'succeeded'
137
96
 
138
- captures_sum += c['amount']
97
+ amount = c['amount']
98
+ amount = amount.to_d if amount.is_a?(String)
99
+ captures_sum += amount
139
100
  end
140
101
  captures_sum.to_d
141
102
  end
142
103
 
143
104
  def flow_io_balance_amount
144
- flow_data&.dig('order', 'balance', 'amount')&.to_d
105
+ flow_data&.dig('order', 'balance', 'amount')&.to_d || 0
145
106
  end
146
107
 
147
108
  def flow_io_payments
148
109
  flow_data.dig('order', 'payments')
149
110
  end
150
111
 
151
- def flow_payment_method
152
- if flow_data['payment_type'] == 'paypal'
153
- 'paypal'
154
- else
155
- 'cc' # creait card is default
156
- end
157
- end
158
-
159
112
  def flow_customer_email
160
113
  flow_data.dig('order', 'customer', 'email')
161
114
  end
@@ -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,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
+ )
@@ -2,58 +2,40 @@
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, if present as @order.user
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: @order.user
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
- class OrderSync # rubocop:disable Metrics/ClassLength
5
+ class OrderSync
18
6
  FLOW_CENTER = 'default'
19
7
 
20
8
  attr_reader :order, :response
21
9
 
22
- delegate :url_helpers, to: 'Rails.application.routes'
23
-
10
+ # @param [Object] order
11
+ # @param [String] flow_session_id
24
12
  def initialize(order:, flow_session_id:)
25
- raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
13
+ raise(ArgumentError, 'Experience not defined or not active') unless order&.zone&.flow_io_active_experience?
26
14
 
27
15
  @experience = order.flow_io_experience_key
28
16
  @flow_session_id = flow_session_id
29
17
  @order = order
30
- @client = FlowcommerceSpree.client(session_id: flow_session_id)
18
+ @client = FlowcommerceSpree.client(default_headers: { "Authorization": "Session #{flow_session_id}" },
19
+ authorization: nil)
31
20
  end
32
21
 
33
22
  # helper method to send complete order from Spree to flow.io
34
23
  def synchronize!
35
- return unless @order.zone&.flow_io_active_experience? && @order.state == 'cart' && @order.line_items.size > 0
24
+ return unless @order.state == 'cart' && @order.line_items.size > 0
36
25
 
37
26
  sync_body!
38
- write_response_in_cache
27
+ write_response_to_order
39
28
 
40
29
  @order.update_columns(total: @order.total, meta: @order.meta.to_json)
41
30
  refresh_checkout_token
42
- @checkout_token
43
- end
44
-
45
- def error
46
- @response['messages'].join(', ')
47
- end
48
-
49
- def error_code
50
- @response['code']
51
31
  end
52
32
 
53
33
  def error?
54
34
  @response&.[]('code') && @response&.[]('messages') ? true : false
55
35
  end
56
36
 
37
+ private
38
+
57
39
  # builds object that can be sent to api.flow.io to sync order data
58
40
  def build_flow_request
59
41
  @opts = { experience: @experience, expand: ['experience'] }
@@ -71,24 +53,21 @@ module FlowcommerceSpree
71
53
  @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency } if @order.adjustment_total != 0
72
54
  end
73
55
 
74
- private
75
-
76
56
  def refresh_checkout_token
77
- root_url = url_helpers.root_url
57
+ root_url = Rails.application.routes.url_helpers.root_url
78
58
  order_number = @order.number
79
59
  confirmation_url = "#{root_url}flow/order-completed?order=#{order_number}&t=#{@order.guest_token}"
80
- @checkout_token = FlowcommerceSpree.client.checkout_tokens.post_checkout_and_tokens_by_organization(
81
- FlowcommerceSpree::ORGANIZATION,
82
- discriminator: 'checkout_token_reference_form',
83
- order_number: order_number,
84
- session_id: @flow_session_id,
85
- urls: { continue_shopping: root_url,
86
- confirmation: confirmation_url,
87
- invalid_checkout: root_url }
88
- )&.id
89
-
90
60
  @order.flow_io_attribute_add('flow_return_url', confirmation_url)
91
61
  @order.flow_io_attribute_add('checkout_continue_shopping_url', root_url)
62
+
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
92
71
  end
93
72
 
94
73
  # if customer is defined, add customer info
@@ -100,13 +79,14 @@ module FlowcommerceSpree
100
79
  customer_ship_address = customer.ship_address
101
80
  address = customer_ship_address if customer_ship_address&.country&.iso3 == @order.zone.flow_io_experience_country
102
81
 
82
+ customer_profile = customer.user_profile
103
83
  unless address
104
- user_profile_address = customer.user_profile&.address
84
+ user_profile_address = customer_profile&.address
105
85
  address = user_profile_address if user_profile_address&.country&.iso3 == @order.zone.flow_io_experience_country
106
86
  end
107
87
 
108
- @body[:customer] = { name: { first: address&.firstname,
109
- last: address&.lastname },
88
+ @body[:customer] = { name: { first: address&.firstname || customer_profile&.first_name,
89
+ last: address&.lastname || customer_profile&.last_name },
110
90
  email: customer.email,
111
91
  number: customer.flow_number,
112
92
  phone: address&.phone }
@@ -116,14 +96,14 @@ module FlowcommerceSpree
116
96
 
117
97
  def add_customer_address(address)
118
98
  streets = []
119
- streets.push address.address1 if address&.address1.present?
120
- streets.push address.address2 if address&.address2.present?
99
+ streets.push address.address1 if address.address1.present?
100
+ streets.push address.address2 if address.address2.present?
121
101
 
122
102
  @body[:destination] = { streets: streets,
123
- city: address&.city,
124
- province: address&.state_name,
125
- postal: address&.zipcode,
126
- country: (address&.country&.iso3 || ''),
103
+ city: address.city,
104
+ province: address.state_name,
105
+ postal: address.zipcode,
106
+ country: (address.country&.iso3 || ''),
127
107
  contact: @body[:customer] }
128
108
 
129
109
  @body[:destination].delete_if { |_k, v| v.nil? }
@@ -132,20 +112,8 @@ module FlowcommerceSpree
132
112
  def sync_body!
133
113
  build_flow_request
134
114
 
135
- @use_get = false
136
-
137
- # use get if order is completed and closed
138
- @use_get = true if @order.flow_data.dig('order', 'submitted_at').present? || @order.state == 'complete'
139
-
140
- # do not use get if there is no local order cache
141
- @use_get = false unless @order.flow_data['order']
142
-
143
- if @use_get
144
- @response ||= @client.orders.get_by_number(ORGANIZATION, @order.number).to_hash
145
- else
146
- @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
147
- Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
148
- end
115
+ @response = @client.orders.put_by_number(ORGANIZATION, @order.number,
116
+ Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
149
117
  end
150
118
 
151
119
  def add_item(line_item)
@@ -160,23 +128,12 @@ module FlowcommerceSpree
160
128
  currency: price_root['currency'] || variant.cost_currency } }
161
129
  end
162
130
 
163
- # set cache for total order amount
164
- # written in flow_data field inside spree_orders table
165
- def write_response_in_cache
166
- if !@response || error?
167
- @order.flow_data.delete('order')
168
- else
169
- response_total = @response[:total]
170
- response_total_label = response_total&.[](:label)
171
- cache_total = @order.flow_data.dig('order', 'total', 'label')
172
-
173
- # return if total is not changed, no products removed or added
174
- return if @use_get && response_total_label == cache_total
175
-
176
- # update local order
177
- @order.total = response_total&.[](:amount)
178
- @order.flow_data.merge!('order' => @response)
179
- end
131
+ def write_response_to_order
132
+ return @order.flow_data.delete('order') if !@response || error?
133
+
134
+ # update local order
135
+ @order.total = @response[:total]&.[](:amount)
136
+ @order.flow_data.merge!('order' => @response)
180
137
  end
181
138
  end
182
139
  end