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