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
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