solidus_backtracs 2.2.0

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.circleci/config.yml +41 -0
  4. data/.gem_release.yml +5 -0
  5. data/.github/stale.yml +17 -0
  6. data/.github_changelog_generator +2 -0
  7. data/.gitignore +20 -0
  8. data/.rspec +2 -0
  9. data/.rubocop.yml +14 -0
  10. data/.rubocop_todo.yml +40 -0
  11. data/CHANGELOG.md +2 -0
  12. data/Gemfile +33 -0
  13. data/LICENSE +26 -0
  14. data/README.md +208 -0
  15. data/Rakefile +6 -0
  16. data/app/assets/javascripts/spree/backend/solidus_backtracs.js +2 -0
  17. data/app/assets/javascripts/spree/frontend/solidus_backtracs.js +2 -0
  18. data/app/assets/stylesheets/spree/backend/solidus_backtracs.css +4 -0
  19. data/app/assets/stylesheets/spree/frontend/solidus_backtracs.css +4 -0
  20. data/app/controllers/spree/backtracs_controller.rb +46 -0
  21. data/app/decorators/models/solidus_backtracs/spree/shipment_decorator.rb +33 -0
  22. data/app/helpers/solidus_backtracs/export_helper.rb +52 -0
  23. data/app/jobs/solidus_backtracs/api/schedule_shipment_syncs_job.rb +28 -0
  24. data/app/jobs/solidus_backtracs/api/sync_shipment_job.rb +17 -0
  25. data/app/jobs/solidus_backtracs/api/sync_shipments_job.rb +41 -0
  26. data/app/queries/solidus_backtracs/shipment/between_query.rb +14 -0
  27. data/app/queries/solidus_backtracs/shipment/exportable_query.rb +24 -0
  28. data/app/queries/solidus_backtracs/shipment/pending_api_sync_query.rb +51 -0
  29. data/app/views/spree/backtracs/export.xml.builder +58 -0
  30. data/bin/console +17 -0
  31. data/bin/rails +7 -0
  32. data/bin/rails-engine +13 -0
  33. data/bin/rails-sandbox +16 -0
  34. data/bin/rake +7 -0
  35. data/bin/sandbox +86 -0
  36. data/bin/setup +8 -0
  37. data/config/locales/en.yml +5 -0
  38. data/config/routes.rb +6 -0
  39. data/db/migrate/20210220093010_add_backtracs_api_sync_fields.rb +8 -0
  40. data/lib/generators/solidus_backtracs/install/install_generator.rb +27 -0
  41. data/lib/generators/solidus_backtracs/install/templates/initializer.rb +91 -0
  42. data/lib/solidus_backtracs/api/batch_syncer.rb +45 -0
  43. data/lib/solidus_backtracs/api/client.rb +36 -0
  44. data/lib/solidus_backtracs/api/rate_limited_error.rb +23 -0
  45. data/lib/solidus_backtracs/api/request_error.rb +33 -0
  46. data/lib/solidus_backtracs/api/request_runner.rb +87 -0
  47. data/lib/solidus_backtracs/api/shipment_serializer.rb +103 -0
  48. data/lib/solidus_backtracs/api/threshold_verifier.rb +28 -0
  49. data/lib/solidus_backtracs/configuration.rb +62 -0
  50. data/lib/solidus_backtracs/engine.rb +19 -0
  51. data/lib/solidus_backtracs/errors.rb +23 -0
  52. data/lib/solidus_backtracs/shipment_notice.rb +58 -0
  53. data/lib/solidus_backtracs/testing_support/factories.rb +4 -0
  54. data/lib/solidus_backtracs/version.rb +5 -0
  55. data/lib/solidus_backtracs.rb +16 -0
  56. data/solidus_shipstation.gemspec +39 -0
  57. data/spec/controllers/spree/backtracs_controller_spec.rb +103 -0
  58. data/spec/fixtures/backtracs_xml_schema.xsd +171 -0
  59. data/spec/jobs/solidus_backtracs/api/schedule_shipment_syncs_job_spec.rb +32 -0
  60. data/spec/jobs/solidus_backtracs/api/sync_shipments_job_spec.rb +102 -0
  61. data/spec/lib/solidus_backtracs/api/batch_syncer_spec.rb +228 -0
  62. data/spec/lib/solidus_backtracs/api/client_spec.rb +120 -0
  63. data/spec/lib/solidus_backtracs/api/rate_limited_error_spec.rb +21 -0
  64. data/spec/lib/solidus_backtracs/api/request_error_spec.rb +20 -0
  65. data/spec/lib/solidus_backtracs/api/request_runner_spec.rb +65 -0
  66. data/spec/lib/solidus_backtracs/api/shipment_serializer_spec.rb +25 -0
  67. data/spec/lib/solidus_backtracs/api/threshold_verifier_spec.rb +61 -0
  68. data/spec/lib/solidus_backtracs/shipment_notice_spec.rb +111 -0
  69. data/spec/lib/solidus_backtracs_spec.rb +9 -0
  70. data/spec/models/spree/shipment_spec.rb +49 -0
  71. data/spec/queries/solidus_backtracs/shipment/between_query_spec.rb +53 -0
  72. data/spec/queries/solidus_backtracs/shipment/exportable_query_spec.rb +53 -0
  73. data/spec/queries/solidus_backtracs/shipment/pending_api_sync_query_spec.rb +37 -0
  74. data/spec/spec_helper.rb +31 -0
  75. data/spec/support/configuration_helper.rb +13 -0
  76. data/spec/support/controllers.rb +1 -0
  77. data/spec/support/webmock.rb +3 -0
  78. data/spec/support/xsd.rb +5 -0
  79. metadata +248 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class SyncShipmentsJob < ApplicationJob
6
+ queue_as :default
7
+
8
+ def perform(shipments)
9
+ shipments = select_shipments(shipments)
10
+ return if shipments.empty?
11
+
12
+ sync_shipments(shipments)
13
+ rescue RateLimitedError => e
14
+ self.class.set(wait: e.retry_in).perform_later
15
+ rescue StandardError => e
16
+ SolidusBacktracs.config.error_handler.call(e, {})
17
+ end
18
+
19
+ private
20
+
21
+ def select_shipments(shipments)
22
+ shipments.select do |shipment|
23
+ if ThresholdVerifier.call(shipment)
24
+ true
25
+ else
26
+ ::Spree::Event.fire(
27
+ 'solidus_backtracs.api.sync_skipped',
28
+ shipment: shipment,
29
+ )
30
+
31
+ false
32
+ end
33
+ end
34
+ end
35
+
36
+ def sync_shipments(shipments)
37
+ BatchSyncer.from_config.call(shipments)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Shipment
5
+ class BetweenQuery
6
+ def self.apply(scope, from:, to:)
7
+ scope.joins(:order).where(<<~SQL.squish, from: from, to: to)
8
+ (spree_shipments.updated_at > :from AND spree_shipments.updated_at < :to) OR
9
+ (spree_orders.updated_at > :from AND spree_orders.updated_at < :to)
10
+ SQL
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Shipment
5
+ class ExportableQuery
6
+ def self.apply(scope)
7
+ scope = scope
8
+ .order(:updated_at)
9
+ .joins(:order)
10
+ .merge(::Spree::Order.complete)
11
+
12
+ unless SolidusBacktracs.configuration.capture_at_notification
13
+ scope = scope.where(spree_shipments: { state: ['ready', 'canceled'] })
14
+ end
15
+
16
+ unless SolidusBacktracs.configuration.export_canceled_shipments
17
+ scope = scope.where.not(spree_shipments: { state: 'canceled' })
18
+ end
19
+
20
+ scope
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Shipment
5
+ class PendingApiSyncQuery
6
+ SQLITE_CONDITION = <<~SQL.squish
7
+ (
8
+ spree_shipments.backtracs_synced_at IS NULL
9
+ ) AND ((JULIANDAY(CURRENT_TIMESTAMP) - JULIANDAY(spree_orders.updated_at)) * 86400.0) < :threshold
10
+ SQL
11
+
12
+ POSTGRES_CONDITION = <<~SQL.squish
13
+ (
14
+ spree_shipments.backtracs_synced_at IS NULL
15
+ ) AND (EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - spree_orders.updated_at))) < :threshold
16
+ SQL
17
+
18
+ MYSQL2_CONDITION = <<~SQL.squish
19
+ (
20
+ spree_shipments.backtracs_synced_at IS NULL
21
+ ) AND (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(spree_orders.updated_at)) < :threshold
22
+ SQL
23
+
24
+ class << self
25
+ def apply(scope)
26
+ scope
27
+ .joins(:order)
28
+ .merge(::Spree::Order.complete)
29
+ .where(condition_for_adapter, threshold: SolidusBacktracs.config.api_sync_threshold / 1.second)
30
+ end
31
+
32
+ private
33
+
34
+ def condition_for_adapter
35
+ db_adapter = ActiveRecord::Base.connection.adapter_name.downcase
36
+
37
+ case db_adapter
38
+ when /sqlite/
39
+ SQLITE_CONDITION
40
+ when /postgres/
41
+ POSTGRES_CONDITION
42
+ when /mysql2/
43
+ MYSQL2_CONDITION
44
+ else
45
+ fail "Backtracs API sync not supported for DB adapter #{db_adapter}!"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ xml = Builder::XmlMarkup.new
4
+ xml.instruct!
5
+ xml.Orders(pages: (@shipments.total_count / 50.0).ceil) {
6
+ @shipments.each do |shipment|
7
+ order = shipment.order
8
+
9
+ xml.Order {
10
+ xml.OrderID shipment.id
11
+ xml.OrderNumber shipment.number # do not use shipment.order.number as this presents lookup issues
12
+ xml.OrderDate order.completed_at.strftime(SolidusBacktracs::ExportHelper::DATE_FORMAT)
13
+ xml.OrderStatus shipment.state
14
+ xml.LastModified [order.completed_at, shipment.updated_at].max.strftime(SolidusBacktracs::ExportHelper::DATE_FORMAT)
15
+ xml.ShippingMethod shipment.shipping_method.try(:name)
16
+ xml.OrderTotal order.total
17
+ xml.TaxAmount order.tax_total
18
+ xml.ShippingAmount order.ship_total
19
+ xml.CustomField1 order.number
20
+
21
+ # if order.gift?
22
+ # xml.Gift
23
+ # xml.GiftMessage
24
+ # end
25
+
26
+ xml.Customer {
27
+ xml.CustomerCode order.email.slice(0, 50)
28
+ SolidusBacktracs::ExportHelper.address(xml, order, :bill)
29
+ SolidusBacktracs::ExportHelper.address(xml, order, :ship)
30
+ }
31
+ xml.Items {
32
+ shipment.line_items.each do |line|
33
+ variant = line.variant
34
+ xml.Item {
35
+ xml.SKU variant.sku
36
+ xml.Name [variant.product.name, variant.options_text].join(' ')
37
+ xml.ImageUrl variant.images.first.try(:attachment).try(:url)
38
+ xml.Weight variant.weight.to_f
39
+ xml.WeightUnits SolidusBacktracs.configuration.weight_units
40
+ xml.Quantity line.quantity
41
+ xml.UnitPrice line.price
42
+
43
+ if variant.option_values.present?
44
+ xml.Options {
45
+ variant.option_values.each do |value|
46
+ xml.Option {
47
+ xml.Name value.option_type.presentation
48
+ xml.Value value.name
49
+ }
50
+ end
51
+ }
52
+ end
53
+ }
54
+ end
55
+ }
56
+ }
57
+ end
58
+ }
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "bundler/setup"
6
+ require "solidus_backtracs"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+ $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ # require "pry"
14
+ # Pry.start
15
+
16
+ require "irb"
17
+ IRB.start(__FILE__)
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if %w[g generate].include? ARGV.first
4
+ exec "#{__dir__}/rails-engine", *ARGV
5
+ else
6
+ exec "#{__dir__}/rails-sandbox", *ARGV
7
+ end
data/bin/rails-engine ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/solidus_backtracs/engine', __dir__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
data/bin/rails-sandbox ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ app_root = 'sandbox'
4
+
5
+ unless File.exist? "#{app_root}/bin/rails"
6
+ warn 'Creating the sandbox app...'
7
+ Dir.chdir "#{__dir__}/.." do
8
+ system "#{__dir__}/sandbox" or begin
9
+ warn 'Automatic creation of the sandbox app failed'
10
+ exit 1
11
+ end
12
+ end
13
+ end
14
+
15
+ Dir.chdir app_root
16
+ exec 'bin/rails', *ARGV
data/bin/rake ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load Gem.bin_path("rake", "rake")
data/bin/sandbox ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ case "$DB" in
6
+ postgres|postgresql)
7
+ RAILSDB="postgresql"
8
+ ;;
9
+ mysql)
10
+ RAILSDB="mysql"
11
+ ;;
12
+ sqlite|'')
13
+ RAILSDB="sqlite3"
14
+ ;;
15
+ *)
16
+ echo "Invalid DB specified: $DB"
17
+ exit 1
18
+ ;;
19
+ esac
20
+
21
+ if [ ! -z $SOLIDUS_BRANCH ]
22
+ then
23
+ BRANCH=$SOLIDUS_BRANCH
24
+ else
25
+ BRANCH="master"
26
+ fi
27
+
28
+ extension_name="solidus_backtracs"
29
+
30
+ # Stay away from the bundler env of the containing extension.
31
+ function unbundled {
32
+ ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
33
+ }
34
+
35
+ rm -rf ./sandbox
36
+ unbundled bundle exec rails new sandbox --database="$RAILSDB" \
37
+ --skip-bundle \
38
+ --skip-git \
39
+ --skip-keeps \
40
+ --skip-rc \
41
+ --skip-spring \
42
+ --skip-test \
43
+ --skip-javascript
44
+
45
+ if [ ! -d "sandbox" ]; then
46
+ echo 'sandbox rails application failed'
47
+ exit 1
48
+ fi
49
+
50
+ cd ./sandbox
51
+ cat <<RUBY >> Gemfile
52
+ gem 'solidus', github: 'solidusio/solidus', branch: '$BRANCH'
53
+ gem 'solidus_auth_devise', '>= 2.1.0'
54
+ gem 'rails-i18n'
55
+ gem 'solidus_i18n'
56
+
57
+ gem '$extension_name', path: '..'
58
+
59
+ group :test, :development do
60
+ platforms :mri do
61
+ gem 'pry-byebug'
62
+ end
63
+ end
64
+ RUBY
65
+
66
+ unbundled bundle install --gemfile Gemfile
67
+
68
+ unbundled bundle exec rake db:drop db:create
69
+
70
+ unbundled bundle exec rails generate solidus:install \
71
+ --auto-accept \
72
+ --user_class=Spree::User \
73
+ --enforce_available_locales=true \
74
+ --with-authentication=false \
75
+ --payment-method=none \
76
+ $@
77
+
78
+ unbundled bundle exec rails generate solidus:auth:install
79
+ unbundled bundle exec rails generate ${extension_name}:install
80
+
81
+ echo
82
+ echo "🚀 Sandbox app successfully created for $extension_name!"
83
+ echo "🚀 Using $RAILSDB and Solidus $BRANCH"
84
+ echo "🚀 Use 'export DB=[postgres|mysql|sqlite]' to control the DB adapter"
85
+ echo "🚀 Use 'export SOLIDUS_BRANCH=<BRANCH-NAME>' to control the Solidus version"
86
+ echo "🚀 This app is intended for test purposes."
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ gem install bundler --conservative
7
+ bundle update
8
+ bin/rake clobber
@@ -0,0 +1,5 @@
1
+ ---
2
+ en:
3
+ shipment_not_found: Shipment %{number} was not found
4
+ import_tracking_error: "Tracking number cannot be imported. Error: %{error}"
5
+ capture_payment_error: "Error in capture of payment for order %{number}. Error: %{error}"
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Core::Engine.routes.draw do
4
+ # get '/backtracs', to: 'backtracs#export'
5
+ # post '/backtracs', to: 'backtracs#shipnotify'
6
+ end
@@ -0,0 +1,8 @@
1
+ # NOTE: This migration is only required if you use the API integration strategy.
2
+ # If you're using the XML file instead, you can safely skip these columns.
3
+
4
+ class AddBacktracsApiSyncFields < ActiveRecord::Migration[5.2]
5
+ def change
6
+ add_column :spree_shipments, :backtracs_synced_at, :datetime
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ class_option :auto_run_migrations, type: :boolean, default: false
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ def copy_initializer
10
+ template 'initializer.rb', 'config/initializers/solidus_backtracs.rb'
11
+ end
12
+
13
+ def add_migrations
14
+ run 'bin/rails railties:install:migrations FROM=solidus_backtracs'
15
+ end
16
+
17
+ def run_migrations
18
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
19
+ if run_migrations
20
+ run 'bin/rails db:migrate'
21
+ else
22
+ puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ SolidusBacktracs.configure do |config|
4
+ # Choose between Grams, Ounces or Pounds.
5
+ config.weight_units = "Grams"
6
+
7
+ # Capture payment when Backtracs notifies a shipping label creation.
8
+ # Set this to `true` and `Spree::Config.require_payment_to_ship` to `false` if you
9
+ # want to charge your customers at the time of shipment.
10
+ config.capture_at_notification = false
11
+
12
+ ## API Configuration
13
+ config.api_base = ENV['BACKTRACS_API_BASE'] || 'https://bactracstest.andlor.com'
14
+
15
+ # Backtracs expects the endpoint to be protected by HTTP Basic Auth.
16
+ # Set the username and password you desire for Backtracs to use.
17
+ config.webhook_username = "smoking_jay_cutler"
18
+ config.webhook_password = "my-awesome-password"
19
+
20
+ ## Proxy
21
+ config.proxy_address = ENV['PROXY_ADDRESS']
22
+ config.proxy_port = ENV['PROXY_PORT']
23
+ config.proxy_username = ENV['PROXY_USER']
24
+ config.proxy_password = ENV['PROXY_PASS']
25
+
26
+ ## Authentication Service Credentials
27
+ config.authentication_username = "red_blue_jay"
28
+ config.authentication_password = "my-secret-other-password"
29
+
30
+
31
+ ## Shipment Serializer Configuration
32
+ config.sku_map = {}
33
+ config.default_rma_type = "W"
34
+ config.default_carrier = "FedExGrnd"
35
+ config.default_ship_method = "GROUND"
36
+ config.default_status = "OPEN"
37
+ config.default_rp_location = "FG-NEW"
38
+ config.shippable_skus = []
39
+ config.default_property_name = "XYZ"
40
+
41
+ ####### XML integration
42
+ # Only uncomment these lines if you're going to use the XML integration.
43
+
44
+ # Export canceled shipments to Backtracs
45
+ # Set this to `true` if you want canceled shipments included in the endpoint.
46
+ # config.export_canceled_shipments = false
47
+
48
+ # You can customize the class used to receive notifications from the POST request
49
+ # Make sure it has a class method `from_payload` which receives the notification hash
50
+ # and an instance method `apply`
51
+ # config.shipment_notice_class = 'SolidusBacktracs::ShipmentNotice'
52
+
53
+ ####### API integration
54
+ # Only uncomment these lines if you're going to use the API integration.
55
+
56
+ # Override the shipment serializer used for API sync. This can be any object
57
+ # that responds to `#call`. At the very least, you'll need to uncomment the
58
+ # following lines and customize your store ID.
59
+ # config.api_shipment_serializer = proc do |shipment|
60
+ # SolidusBacktracs::Api::ShipmentSerializer.new(store_id: '12345678').call(shipment)
61
+ # end
62
+
63
+ # Override the logic used to match a Backtracs order to a shipment from a
64
+ # given collection. This can be useful when you override the default serializer
65
+ # and change the logic used to generate the order number.
66
+ # config.api_shipment_matcher = proc do |backtracs_order, shipments|
67
+ # shipments.find { |shipment| shipment.number == backtracs_order['orderNumber'] }
68
+ # end
69
+
70
+ # API key and secret for accessing the Backtracs API.
71
+ # config.api_key = "api-key"
72
+ # config.api_secret = "api-secret"
73
+
74
+ # Number of shipments to import into Backtracs at once.
75
+ # If unsure, leave this set to 100, which is the maximum
76
+ # number of shipments that can be imported at once.
77
+ config.api_batch_size = 100
78
+
79
+ # Period of time after which the integration will "drop" shipments and stop
80
+ # trying to create/update them. This prevents the API from retrying indefinitely
81
+ # in case an error prevents some shipments from being created/updated.
82
+ config.api_sync_threshold = 7.days
83
+
84
+ # Error handler used by the API integration for certain non-critical errors (e.g.
85
+ # a failure when serializing a shipment from a batch). This should be a proc that
86
+ # accepts an exception and a context hash. Popular options for error handling are
87
+ # logging or sending the error to an error tracking tool such as Sentry.
88
+ config.error_handler = -> (error, context = {}) {
89
+ Sentry.capture_exception(error, extra: context)
90
+ }
91
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class BatchSyncer
6
+ class << self
7
+ def from_config
8
+ new(
9
+ client: SolidusBacktracs::Api::Client.from_config,
10
+ shipment_matcher: SolidusBacktracs.config.api_shipment_matcher,
11
+ )
12
+ end
13
+ end
14
+
15
+ attr_reader :client, :shipment_matcher
16
+
17
+ def initialize(client:, shipment_matcher:)
18
+ @client = client
19
+ @shipment_matcher = shipment_matcher
20
+ end
21
+
22
+ def call(shipments)
23
+ begin
24
+ response = client.bulk_create_orders(shipments)
25
+ rescue RateLimitedError => e
26
+ ::Spree::Event.fire(
27
+ 'solidus_backtracs.api.rate_limited',
28
+ shipments: shipments,
29
+ error: e,
30
+ )
31
+
32
+ raise e
33
+ rescue RequestError => e
34
+ ::Spree::Event.fire(
35
+ 'solidus_backtracs.api.sync_errored',
36
+ shipments: shipments,
37
+ error: e,
38
+ )
39
+
40
+ raise e
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class Client
6
+ class << self
7
+ def from_config
8
+ new(
9
+ request_runner: RequestRunner.new,
10
+ error_handler: SolidusBacktracs.config.error_handler,
11
+ shipment_serializer: SolidusBacktracs.config.api_shipment_serializer,
12
+ )
13
+ end
14
+ end
15
+
16
+ attr_reader :request_runner, :error_handler, :shipment_serializer
17
+
18
+ def initialize(request_runner:, error_handler:, shipment_serializer:)
19
+ @request_runner = request_runner
20
+ @error_handler = error_handler
21
+ @shipment_serializer = shipment_serializer
22
+ end
23
+
24
+ def bulk_create_orders(shipments)
25
+ shipments.each do |shipment|
26
+ SolidusBacktracs::Api::SyncShipmentJob.perform_now(
27
+ shipment_id: shipment.id,
28
+ error_handler: @error_handler,
29
+ shipment_serializer: @shipment_serializer,
30
+ request_runner: @request_runner
31
+ )
32
+ end.compact
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class RateLimitedError < RequestError
6
+ attr_reader :retry_in
7
+
8
+ class << self
9
+ def options_from_response(response)
10
+ super.merge(
11
+ retry_in: response.headers['X-Rate-Limit-Reset'].to_i.seconds,
12
+ )
13
+ end
14
+ end
15
+
16
+ def initialize(retry_in:, **options)
17
+ super(**options)
18
+
19
+ @retry_in = retry_in
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class RequestError < RuntimeError
6
+ attr_reader :response_code, :response_body, :response_headers
7
+
8
+ class << self
9
+ def from_response(response)
10
+ new(**options_from_response(response))
11
+ end
12
+
13
+ private
14
+
15
+ def options_from_response(response)
16
+ {
17
+ response_code: response.code,
18
+ response_headers: response.headers,
19
+ response_body: response.body,
20
+ }
21
+ end
22
+ end
23
+
24
+ def initialize(response_code:, response_body:, response_headers:)
25
+ @response_code = response_code
26
+ @response_body = response_body
27
+ @response_headers = response_headers
28
+
29
+ super(response_body)
30
+ end
31
+ end
32
+ end
33
+ end