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,87 @@
1
+ # frozen_string_literal: true
2
+ require "uri"
3
+ require "net/http"
4
+
5
+ module SolidusBacktracs
6
+ module Api
7
+ class RequestRunner
8
+
9
+ def initialize
10
+ @username = SolidusBacktracs.configuration.authentication_username
11
+ @password = SolidusBacktracs.configuration.authentication_password
12
+ @api_base = SolidusBacktracs.configuration.api_base
13
+ end
14
+
15
+ def authenticated_call(method: nil, path: nil, serializer: nil, shipment: nil)
16
+ unless @username.present? || @password.present? || @api_base.present?
17
+ raise "Credentials not defined for Authentication"
18
+ end
19
+
20
+ Rails.cache.fetch("backtracks_cache_key", expires_in: 1.hour) do
21
+ @response = self.call(method: :get, path: "/webservices/user/Authentication.asmx/Login?sUserName=#{@username}&sPassword=#{@password}")
22
+ end
23
+
24
+ authenticted = parse_authentication_response(@response, "Result")
25
+
26
+ if authenticted == 'false'
27
+ raise RequestError.from_response(@response)
28
+ else
29
+ sguid = parse_authentication_response(@response, "Message")
30
+ params = serializer.call(shipment, sguid)
31
+
32
+ rma_response = call(method: :post, path: path, params: params)
33
+ sync_shipment(shipment, rma_response)
34
+ end
35
+ end
36
+
37
+ def call(method: nil, path: nil, params: {})
38
+ doc = {}
39
+ if params.present?
40
+ doc = Nokogiri::XML(params.to_s)
41
+ end
42
+ response = HTTParty.send(
43
+ method,
44
+ URI.join(@api_base, path),
45
+ body: doc.to_xml,
46
+ http_proxyaddr: SolidusBacktracs.configuration.proxy_address,
47
+ http_proxyport: SolidusBacktracs.configuration.proxy_port,
48
+ http_proxyuser: SolidusBacktracs.configuration.proxy_username,
49
+ http_proxypass: SolidusBacktracs.configuration.proxy_password,
50
+ headers: {
51
+ 'Content-Type' => 'text/xml',
52
+ },
53
+ )
54
+
55
+ case response.code.to_s
56
+ when /2\d{2}/
57
+ response
58
+ when '429'
59
+ raise RateLimitedError.from_response(response)
60
+ else
61
+ raise RequestError.from_response(response)
62
+ end
63
+ end
64
+
65
+ def parse_authentication_response(response, type)
66
+ response.dig("AuthenticationResponse", type)
67
+ end
68
+
69
+ def sync_shipment(shipment, response)
70
+ result = response.dig("Envelope", "Body", "CreateNewResponse", "CreateNewResult", "Result")
71
+ if result == 'true'
72
+ shipment.update_column(:backtracs_synced_at, Time.zone.now)
73
+
74
+ ::Spree::Event.fire(
75
+ 'solidus_backtracs.api.sync_completed',
76
+ shipment: shipment
77
+ )
78
+ else
79
+ ::Spree::Event.fire(
80
+ 'solidus_backtracs.api.sync_failed',
81
+ shipment: shipment
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class ShipmentSerializer
6
+
7
+ def initialize(shipment:)
8
+ @shipment = shipment
9
+ @config = SolidusBacktracs.config
10
+ @property_id = ::Spree::Property.find_by(name: SolidusBacktracs.config.default_property_name)&.id
11
+ end
12
+
13
+ def call(sguid: nil)
14
+ order = @shipment.order
15
+ user = @shipment.user
16
+
17
+ xml = Builder::XmlMarkup.new
18
+ xml.instruct!(:xml, :encoding => "UTF-8")
19
+
20
+ xml.soap(:Envelope, {"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema", "xmlns:soap" => "http://schemas.xmlsoap.org/soap/envelope/"}) do
21
+ xml.soap :Body do
22
+ xml.CreateNew({"xmlns" => "http://bactracs.andlor.com/rmaservice"}) do
23
+ xml.sGuid sguid
24
+ xml.NewRMA {
25
+ xml.RMANumber @shipment.number
26
+ xml.RMATypeName @config.default_rma_type
27
+ xml.RMASubTypeName
28
+ xml.CustomerRef
29
+ xml.InboundShippingPriority
30
+ xml.InboundTrackingNumber @shipment.tracking
31
+
32
+ xml.Ship {
33
+ xml.Carrier @config.default_carrier
34
+ xml.ShipMethod @config.default_ship_method
35
+ xml.ShipDate @shipment.created_at.strftime(SolidusBacktracs::ExportHelper::BACTRACS_DATE_FORMAT)
36
+ xml.TrackingNumber @shipment.tracking
37
+ xml.SerialNumber @shipment.number
38
+ xml.Ud1
39
+ }
40
+ xml.Customer {
41
+ SolidusBacktracs::ExportHelper.backtracs_address(xml, order, :ship)
42
+ SolidusBacktracs::ExportHelper.backtracs_address(xml, order, :bill)
43
+ }
44
+ xml.Rep {
45
+ xml.Code
46
+ xml.Name user.full_name
47
+ xml.Email user.email
48
+ }
49
+
50
+ xml.RMALines {
51
+ @shipment.line_items.each do |line|
52
+ product = line.product
53
+ if product.respond_to?(:assembly?) && product.assembly?
54
+ product.parts.each do |part|
55
+ next unless part.product.product_properties.where(property_id: @property_id).present?
56
+ line_items_xml(xml: xml, line_item: line, variant: part, order: order)
57
+ end
58
+ else
59
+ line_items_xml(xml: xml, line_item: line, variant: line.variant, order: order)
60
+ end
61
+ end
62
+ }
63
+ xml.OrderDate order.completed_at.strftime(SolidusBacktracs::ExportHelper::BACTRACS_DATE_FORMAT)
64
+ xml.CreateDate @shipment.created_at.strftime(SolidusBacktracs::ExportHelper::BACTRACS_DATE_FORMAT)
65
+ xml.Status @config.default_status
66
+ xml.RMAId @shipment.id
67
+ xml.ClientGuid
68
+ }
69
+ end
70
+ end
71
+ end
72
+ xml
73
+ end
74
+
75
+ def line_items_xml(xml: nil, line_item: nil, variant: nil, order: nil)
76
+ shipment_notice = @shipment.shipment_notice
77
+ xml.RMALine {
78
+ xml.DFItem find_sku_variant(variant)
79
+ xml.DFModelNum find_sku_variant(variant)
80
+ xml.DFCategory
81
+ xml.DFCategoryDescription
82
+ xml.DFQuantity line_item.quantity
83
+ xml.DFUnitPrice line_item.price
84
+ xml.DFSerialNumbers
85
+ xml.Ud1s
86
+ xml.CurrentWarranties
87
+ xml.DFComments
88
+ xml.DFStatus @shipment.state
89
+ xml.PurchaseDate order.completed_at.strftime(SolidusBacktracs::ExportHelper::BACTRACS_DATE_FORMAT)
90
+ xml.ServiceProvider shipment_notice&.service
91
+ xml.WarrantyRepair
92
+ xml.RMALineTest
93
+ xml.InboundShipWeight variant.weight.to_f
94
+ xml.RPLocation @config.default_rp_location
95
+ }
96
+ end
97
+
98
+ def find_sku_variant(variant)
99
+ @config.sku_map[variant.sku].present? ? @config.sku_map[variant.sku] : variant.sku
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ module Api
5
+ class ThresholdVerifier
6
+ class << self
7
+ def call(shipment)
8
+ return false unless shipment.order.completed?
9
+
10
+ !!(shipment_requires_creation?(shipment) || shipment_requires_update?(shipment))
11
+ end
12
+
13
+ private
14
+
15
+ def shipment_requires_creation?(shipment)
16
+ shipment.backtracs_synced_at.nil? &&
17
+ Time.zone.now - shipment.order.updated_at < SolidusBacktracs.config.api_sync_threshold
18
+ end
19
+
20
+ def shipment_requires_update?(shipment)
21
+ shipment.backtracs_synced_at &&
22
+ shipment.backtracs_synced_at < shipment.order.updated_at &&
23
+ Time.zone.now - shipment.order.updated_at < SolidusBacktracs.config.api_sync_threshold
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ class Configuration
5
+ attr_accessor(
6
+ :webhook_username,
7
+ :webhook_password,
8
+ :weight_units,
9
+ :ssl_encrypted,
10
+ :capture_at_notification,
11
+ :export_canceled_shipments,
12
+ :api_batch_size,
13
+ :api_sync_threshold,
14
+ :api_shipment_serializer,
15
+ :api_key,
16
+ :api_secret,
17
+ :api_shipment_matcher,
18
+ :error_handler,
19
+ :shipment_notice_class,
20
+ :authentication_username,
21
+ :authentication_password,
22
+ :api_base,
23
+ :proxy_address,
24
+ :proxy_port,
25
+ :proxy_username,
26
+ :proxy_password,
27
+ :default_carrier,
28
+ :default_ship_method,
29
+ :default_rp_location,
30
+ :default_status,
31
+ :default_property_name,
32
+ :sku_map,
33
+ :default_rma_type,
34
+ :shippable_skus
35
+ )
36
+
37
+ def initialize
38
+ @api_batch_size = 100
39
+ @api_sync_threshold = 7.days
40
+ @error_handler = ->(_error, _extra = {}) {
41
+ Rails.logger.error "#{error.inspect} (#{extra.inspect})"
42
+ }
43
+ @api_shipment_matcher = proc do |backtracs_order, shipments|
44
+ shipments.find { |shipment| shipment.number == backtracs_order['orderNumber'] }
45
+ end
46
+
47
+ @shipment_notice_class = 'SolidusBacktracs::ShipmentNotice'
48
+ end
49
+ end
50
+
51
+ class << self
52
+ def configuration
53
+ @configuration ||= Configuration.new
54
+ end
55
+
56
+ alias config configuration
57
+
58
+ def configure
59
+ yield configuration
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_core'
4
+ require 'solidus_support'
5
+
6
+ module SolidusBacktracs
7
+ class Engine < Rails::Engine
8
+ include SolidusSupport::EngineExtensions
9
+
10
+ isolate_namespace ::Spree
11
+
12
+ engine_name 'solidus_backtracs'
13
+
14
+ # use rspec for tests
15
+ config.generators do |g|
16
+ g.test_framework :rspec
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ class Error < StandardError; end
5
+
6
+ class OrderNotPaidError < Error
7
+ def initialize(order, *args)
8
+ super("Order #{order.number} is not paid and capture_at_notification is false", *args)
9
+ end
10
+ end
11
+
12
+ class ShipmentNotFoundError < Error
13
+ def initialize(shipment_number, *args)
14
+ super("Could not find shipment with number #{shipment_number}", *args)
15
+ end
16
+ end
17
+
18
+ class PaymentError < Error
19
+ def initialize(payment, *args)
20
+ super("Could not process payment #{payment.id}", *args)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ class ShipmentNotice
5
+ attr_reader :shipment_number, :shipment_tracking
6
+
7
+ class << self
8
+ def from_payload(params)
9
+ new(
10
+ shipment_number: params[:order_number],
11
+ shipment_tracking: params[:tracking_number],
12
+ )
13
+ end
14
+ end
15
+
16
+ def initialize(shipment_number:, shipment_tracking:)
17
+ @shipment_number = shipment_number
18
+ @shipment_tracking = shipment_tracking
19
+ end
20
+
21
+ def apply
22
+ unless shipment
23
+ raise ShipmentNotFoundError, shipment
24
+ end
25
+
26
+ process_payment
27
+ ship_shipment
28
+
29
+ shipment
30
+ end
31
+
32
+ private
33
+
34
+ def shipment
35
+ @shipment ||= ::Spree::Shipment.find_by(number: shipment_number)
36
+ end
37
+
38
+ def process_payment
39
+ return if shipment.order.paid?
40
+
41
+ unless SolidusBacktracs.configuration.capture_at_notification
42
+ raise OrderNotPaidError, shipment.order
43
+ end
44
+
45
+ shipment.order.payments.pending.each do |payment|
46
+ payment.capture!
47
+ rescue ::Spree::Core::GatewayError
48
+ raise PaymentError, payment
49
+ end
50
+ end
51
+
52
+ def ship_shipment
53
+ shipment.update!(tracking: shipment_tracking)
54
+ shipment.ship! if shipment.can_ship?
55
+ shipment.order.recalculate
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBacktracs
4
+ VERSION = '2.2.0'
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+
5
+ require 'solidus_backtracs/api/batch_syncer'
6
+ require 'solidus_backtracs/api/request_runner'
7
+ require 'solidus_backtracs/api/client'
8
+ require 'solidus_backtracs/api/request_error'
9
+ require 'solidus_backtracs/api/rate_limited_error'
10
+ require 'solidus_backtracs/api/shipment_serializer'
11
+ require 'solidus_backtracs/api/threshold_verifier'
12
+ require 'solidus_backtracs/configuration'
13
+ require 'solidus_backtracs/errors'
14
+ require 'solidus_backtracs/shipment_notice'
15
+ require 'solidus_backtracs/version'
16
+ require 'solidus_backtracs/engine'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/solidus_backtracs/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'solidus_backtracs'
7
+ spec.version = SolidusBacktracs::VERSION
8
+ spec.authors = ['Stephen Puiszis']
9
+ spec.email = 'steve@tablexi.com'
10
+
11
+ spec.summary = 'A Solidus extension for integrating the Backtracs API.'
12
+ spec.homepage = 'https://github.com/solidusio-contrib/solidus_backtracs'
13
+ spec.license = 'BSD-3-Clause'
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://github.com/solidusio-contrib/solidus_backtracs'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/solidusio-contrib/solidus_backtracs/blob/master/CHANGELOG.md'
18
+
19
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
24
+
25
+ spec.files = files.grep_v(%r{^(test|spec|features)/})
26
+ spec.test_files = files.grep(%r{^(test|spec|features)/})
27
+ spec.bindir = "exe"
28
+ spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency 'httparty', '~> 0.18'
32
+ spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 4']
33
+ spec.add_dependency 'solidus_support', '~> 0.5'
34
+
35
+ spec.add_development_dependency 'rails-controller-testing'
36
+ spec.add_development_dependency 'rspec-xsd'
37
+ spec.add_development_dependency 'solidus_dev_support', '~> 2.5'
38
+ spec.add_development_dependency 'webmock'
39
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Spree::BacktracsController do
4
+ render_views
5
+
6
+ describe '#export' do
7
+ context 'when the authentication is invalid' do
8
+ it 'returns an error error' do
9
+ get :export, params: { format: 'xml' }
10
+
11
+ expect(response.status).to eq(401)
12
+ end
13
+ end
14
+
15
+ context 'when the authentication is valid' do
16
+ it 'responds with 200 OK' do
17
+ stub_backtracs_auth
18
+ create(:order_ready_to_ship)
19
+
20
+ get :export,
21
+ params: {
22
+ start_date: 1.day.ago.strftime('%m/%d/%Y %H:%M'),
23
+ end_date: 1.day.from_now.strftime('%m/%d/%Y %H:%M'),
24
+ format: 'xml'
25
+ }
26
+
27
+ expect(response.status).to eq(200)
28
+ end
29
+
30
+ it 'generates Backtracs-compliant XML' do
31
+ stub_backtracs_auth
32
+ create(:order_ready_to_ship)
33
+
34
+ get :export, params: {
35
+ start_date: 1.day.ago.strftime('%m/%d/%Y %H:%M'),
36
+ end_date: 1.day.from_now.strftime('%m/%d/%Y %H:%M'),
37
+ format: 'xml'
38
+ }
39
+
40
+ expect(response.body).to pass_validation('spec/fixtures/backtracs_xml_schema.xsd')
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#shipnotify' do
46
+ context 'when the authentication is valid' do
47
+ context 'when the shipment can be found' do
48
+ it 'responds with 200 OK' do
49
+ stub_backtracs_auth
50
+ shipment = create(:order_ready_to_ship).shipments.first
51
+
52
+ post :shipnotify, params: {
53
+ order_number: shipment.number,
54
+ tracking_number: '123456',
55
+ format: 'xml',
56
+ }
57
+ shipment.reload
58
+
59
+ expect(response.status).to eq(200)
60
+ end
61
+
62
+ it 'updates the shipment' do
63
+ stub_backtracs_auth
64
+ shipment = create(:order_ready_to_ship).shipments.first
65
+
66
+ post :shipnotify, params: {
67
+ order_number: shipment.number,
68
+ tracking_number: '123456',
69
+ format: 'xml',
70
+ }
71
+ shipment.reload
72
+
73
+ expect(shipment).to have_attributes(
74
+ tracking: '123456',
75
+ state: 'shipped',
76
+ shipped_at: an_instance_of(ActiveSupport::TimeWithZone),
77
+ )
78
+ end
79
+ end
80
+
81
+ context 'when the shipment cannot be found' do
82
+ it 'responds with 400 Bad Request' do
83
+ stub_backtracs_auth
84
+ shipment = create(:order_ready_to_ship).shipments.first
85
+
86
+ post :shipnotify, params: {
87
+ order_number: 'ABC123',
88
+ tracking_number: '123456',
89
+ format: 'xml',
90
+ }
91
+ shipment.reload
92
+
93
+ expect(response.status).to eq(400)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def stub_backtracs_auth(username = 'mario', password = 'lemieux')
100
+ stub_configuration(username: username, password: password)
101
+ request.headers['Authorization'] = ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
102
+ end
103
+ end