solidus_backtracs 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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