dhl_express_global 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 25e0e751ae94b63b522255dbf5845e0fd29cf0da
4
+ data.tar.gz: 547a4abace99495614dfbfbdb37535975949eefb
5
+ SHA512:
6
+ metadata.gz: 8ca2bcad9315a1ec247841d40c2a2648199186fde8de11742e0627a4fd81ace783cebd20b408249bf3fc22c760537a0a01427f6120ec4f8c42608e3973b4ba44
7
+ data.tar.gz: c89c3c5df237c6d5235d922b880f43c0e3c74139048c48a604efe9ce0319ffe872e23dd8eb514d7eceb863f2232936cf247dc182a864558200ebba03c1b59800
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ dhl_credentials.yml
16
+ spec/vcr
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dhl_express_global.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 JONBRWN
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # DhlExpressGlobal
2
+
3
+ Ruby API wrapper to interface with DHL Express Global
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'dhl_express_global'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install dhl_express_global
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Testing / Rspec
26
+
27
+ This gem is using Rspec for testing. As well VCR is implemented for testing the API. To make VCR work make sure to copy `spec/config/dhl_credentials.example.yml` to `spec/config/dhl_credentials.yml` and update the development credentails with actual account details.
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it ( https://github.com/[my-github-username]/dhl_express_global/fork )
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dhl_express_global/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dhl_express_global"
8
+ spec.version = DhlExpressGlobal::VERSION
9
+ spec.authors = ["JONBRWN"]
10
+ spec.email = ["jonathanbrown.a@gmail.com"]
11
+ spec.summary = %q{Ruby wrapper for the DHL Express Global API}
12
+ spec.description = %q{Ruby wrapper for the DHL Express Global API}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'httparty', '>= 0.8.3'
22
+ spec.add_dependency 'nokogiri', '>= 1.5.6'
23
+ spec.add_dependency 'builder'
24
+ spec.add_dependency 'xml-simple'
25
+ spec.add_dependency 'activesupport'
26
+
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.7"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency 'rspec', '~> 3.0.0'
31
+ spec.add_development_dependency 'vcr', '~> 2.00'
32
+ spec.add_development_dependency 'webmock', '~> 1.8.0'
33
+ spec.add_development_dependency 'pry'
34
+ spec.add_development_dependency 'debugger'
35
+ end
@@ -0,0 +1,7 @@
1
+ require 'dhl_express_global/shipment'
2
+
3
+ module DhlExpressGlobal
4
+ require 'dhl_express_global/version'
5
+ #Exceptions: DhlExpressGlobal::RateError
6
+ class RateError < StandardError; end
7
+ end
@@ -0,0 +1,16 @@
1
+ require 'dhl_express_global/helpers'
2
+
3
+ module DhlExpressGlobal
4
+ class Credentials
5
+ include Helpers
6
+ attr_reader :username, :password, :account_number, :mode
7
+
8
+ def initialize(options = {})
9
+ requires!(options, :username, :password, :account_number, :mode)
10
+ @username = options[:username]
11
+ @password = options[:password]
12
+ @account_number = options[:account_number]
13
+ @mode = options[:mode]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module DhlExpressGlobal
2
+ module Helpers
3
+
4
+ private
5
+ # String or :symbol to CamelCase
6
+ def camelize(s)
7
+ # s.to_s.split('_').map { |e| e.capitalize }.join('')
8
+ s.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
9
+ end
10
+
11
+ # Helper method to validate required fields
12
+ def requires!(hash, *params)
13
+ params.each { |param| raise RateError, "Missing Required Parameter #{param}" if hash[param].nil? }
14
+ end
15
+
16
+ def underscorize(key) #:nodoc:
17
+ key.to_s.sub(/^(v[0-9]+|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ require 'base64'
2
+ require 'pathname'
3
+
4
+ module DhlExpressGlobal
5
+ class Label
6
+ attr_accessor :options, :image, :response_details
7
+
8
+ def initialize(label_details = {})
9
+ @response_details = label_details[:envelope][:body][:shipment_response]
10
+ @options = @response_details[:label_image]
11
+ @options[:tracking_number] = @response_details[:packages_result][:package_result][:tracking_number]
12
+ @options[:file_name] = label_details[:file_name]
13
+ @image = Base64.decode64(options[:graphic_image]) if has_image?
14
+
15
+ if file_name = @options[:file_name]
16
+ save(file_name, false)
17
+ end
18
+ end
19
+
20
+ def name
21
+ [tracking_number, format].join('.')
22
+ end
23
+
24
+ def format
25
+ options[:label_image_format]
26
+ end
27
+
28
+ def file_name
29
+ options[:file_name]
30
+ end
31
+
32
+ def tracking_number
33
+ options[:tracking_number]
34
+ end
35
+
36
+ def save(path, append_name = true)
37
+ return unless has_image?
38
+
39
+ full_path = Pathname.new(path)
40
+ full_path = full_path.join(name) if append_name
41
+
42
+ File.open(full_path, 'wb') do |f|
43
+ f.write(@image)
44
+ end
45
+ end
46
+
47
+ def has_image?
48
+ options.key?(:graphic_image)
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,157 @@
1
+ require 'httparty'
2
+ require 'nokogiri'
3
+ require 'active_support/core_ext/hash'
4
+ require 'dhl_express_global/helpers'
5
+
6
+ module DhlExpressGlobal
7
+ module Request
8
+ class Base
9
+ include Helpers
10
+ include HTTParty
11
+ format :xml
12
+
13
+ attr_accessor :debug
14
+
15
+ default_options.update(verify: false)
16
+
17
+ # DHL Express Global Test URL
18
+ TEST_URL = 'https://wsb.dhl.com:443/sndpt/expressRateBook?WSDL'
19
+
20
+ # DHL Express Global Production URL
21
+ PRODUCTION_URL = ''
22
+
23
+ # SERVICE_CODES = []
24
+
25
+ # List of Payment Info codes
26
+ PAYMENT_INFO_CODES = ["CFR", "CIF", "CIP", "CPT", "DAF", "DDP", "DDU", "DAP", "DEQ", "DES", "EXW", "FAS", "FCA", "FOB"]
27
+
28
+
29
+ def initialize(credentials, options = {})
30
+ requires!(options, :shipper, :recipient, :packages)
31
+ @credentials = credentials
32
+ @shipper, @recipient, @packages, @service_type, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:debug]
33
+ @debug = ENV['DEBUG'] == 'true'
34
+ @shipping_options = options[:shipping_options] ||= {}
35
+ @payment_options = options[:payment_options] ||= {}
36
+ end
37
+
38
+ def process_request
39
+ raise NotImplementedError, 'Override #process_request in subclass'
40
+ end
41
+
42
+ def api_url
43
+ @credentials.mode == 'production' ? PRODUCTION_URL : TEST_URL
44
+ end
45
+
46
+ def build_xml
47
+ raise NotImplementedError, 'Override #build_xml in subclass'
48
+ end
49
+
50
+ def parse_response(response)
51
+ response = Hash.from_xml( response.parsed_response.gsub("\n", "") ) if response.parsed_response.is_a? String
52
+ response = sanitize_response_keys(response)
53
+ end
54
+
55
+ def sanitize_response_keys(response)
56
+ if response.is_a? Hash
57
+ response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
58
+ elsif response.is_a? Array
59
+ response.collect { |result| sanitize_response_keys(result) }
60
+ else
61
+ response
62
+ end
63
+ end
64
+
65
+ def add_ws_authentication_header(xml)
66
+ xml[:soapenv].Header {
67
+ xml[:wsse].Security('soapenv:mustUnderstand' => "1" ,
68
+ 'xmlns:wsse' => 'http://schemas.xmlsoap.org/ws/2003/06/secext',
69
+ 'xmlns:wsu' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd') {
70
+ xml[:wsse].UsernameToken('wsu:Id' => 'UsernameToken') {
71
+ xml[:wsse].Username @credentials.username
72
+ xml[:wsse].Password(@credentials.password, 'Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText')
73
+ }
74
+ }
75
+ }
76
+ end
77
+
78
+ def add_shipper(xml)
79
+ xml.Shipper {
80
+ xml.Contact {
81
+ xml.PersonName @shipper[:name]
82
+ xml.CompanyName @shipper[:company]
83
+ xml.PhoneNumber @shipper[:phone_number]
84
+ }
85
+ xml.Address {
86
+ add_address_street_lines(xml, @shipper[:address])
87
+ xml.City @shipper[:city]
88
+ xml.PostalCode @shipper[:postal_code]
89
+ xml.StateOrProvinceCode @shipper[:state] if @shipper[:state]
90
+ xml.CountryCode @shipper[:country_code]
91
+ }
92
+ }
93
+ end
94
+
95
+ def add_recipient(xml)
96
+ xml.Recipient {
97
+ xml.Contact {
98
+ xml.PersonName @recipient[:name]
99
+ xml.CompanyName @recipient[:company]
100
+ xml.PhoneNumber @recipient[:phone_number]
101
+ }
102
+ xml.Address {
103
+ add_address_street_lines(xml, @recipient[:address])
104
+ xml.City @recipient[:city]
105
+ xml.PostalCode @recipient[:postal_code]
106
+ xml.StateOrProvinceCode @recipient[:state] if @recipient[:state]
107
+ xml.CountryCode @recipient[:country_code]
108
+ }
109
+ }
110
+ end
111
+
112
+ def add_address_street_lines(xml, address)
113
+ Array(address).take(3).each_with_index do |address_line, i|
114
+ case i
115
+ when 0
116
+ xml.StreetLines address_line
117
+ when 1
118
+ xml.StreetLines2 address_line
119
+ when 2
120
+ xml.streetLines3 address_line
121
+ end
122
+ end
123
+ end
124
+
125
+ def add_requested_packages(xml)
126
+ @packages.each_with_index do |package, i|
127
+ xml.RequestedPackages('number' => i + 1) {
128
+ xml.Weight package[:weight][:value]
129
+ xml.Dimensions {
130
+ xml.Length package[:dimensions][:length]
131
+ xml.Width package[:dimensions][:width]
132
+ xml.Height package[:dimensions][:height]
133
+ }
134
+ }
135
+ end
136
+ xml.ShipTimestamp (Time.now + 10*60).strftime("%Y-%m-%dT%H:%M:%SGMT%:z")
137
+ xml.UnitOfMeasurement @packages.first[:weight][:units] == 'KG' ? 'SI' : 'SU'
138
+ xml.Content @shipping_options[:package_contents] ||= "NON_DOCUMENTS"
139
+ end
140
+
141
+ # Add information for shipments
142
+ def add_shipment_info(xml)
143
+ xml.ShipmentInfo {
144
+ xml.DropOffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
145
+ xml.ServiceType @shipping_options[:service_type]
146
+ xml.RequestValueAddedServices @shipping_options[:request_value_added_services] ||= "N"
147
+ xml.NextBusinessDay @shipping_options[:next_business_day] ||= "N"
148
+ }
149
+ end
150
+
151
+ def headers
152
+ {"Content-Type"=>"text/xml; charset=utf-8"}
153
+ end
154
+
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,77 @@
1
+ require 'dhl_express_global/request/base'
2
+
3
+ module DhlExpressGlobal
4
+ module Request
5
+ class Delete < Base
6
+
7
+ attr_reader :pickup_date, :pickup_country, :dispatch_confirmation_number, :requestor_name, :reason_code
8
+
9
+ def initialize(credentials, options = {})
10
+ requires!(options, :pickup_date, :pickup_country, :dispatch_confirmation_number, :requestor_name)
11
+ @credentials = credentials
12
+ @pickup_date, @pickup_country, @dispatch_confirmation_number, @requestor_name = options[:pickup_date], options[:pickup_country], options[:dispatch_confirmation_number], options[:requestor_name]
13
+ @reason_code = options[:reason_code] || "001"
14
+ end
15
+
16
+ def process_request
17
+ api_response = self.class.post api_url, :body => build_xml, :headers => headers
18
+ puts api_response if @debug
19
+ response = parse_response(api_response)
20
+ unless success?(response)
21
+ failure_response(response)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def build_xml
28
+ builder = Nokogiri::XML::Builder.new do |xml|
29
+ xml[:soapenv].Envelope( 'xmlns:soapenv' => "http://schemas.xmlsoap.org/soap/envelope/",
30
+ 'xmlns:del' => "http://scxgxtt.phx-dc.dhl.com/euExpressRateBook/ShipmentMsgRequest") {
31
+ add_ws_authentication_header(xml)
32
+ xml[:soapenv].Body {
33
+ xml[:del].DeleteRequest {
34
+ xml.PickupDate {
35
+ xml.parent.namespace = nil
36
+ xml.text pickup_date
37
+ }
38
+ xml.PickupCountry {
39
+ xml.parent.namespace = nil
40
+ xml.text pickup_country
41
+ }
42
+ xml.DispatchConfirmationNumber {
43
+ xml.parent.namespace = nil
44
+ xml.text dispatch_confirmation_number
45
+ }
46
+ xml.RequestorName {
47
+ xml.parent.namespace = nil
48
+ xml.text requestor_name
49
+ }
50
+ xml.Reason {
51
+ xml.parent.namespace = nil
52
+ xml.text reason_code
53
+ }
54
+ }
55
+ }
56
+ }
57
+ end
58
+ builder.doc.root.to_xml
59
+ end
60
+
61
+ def failure_response(response)
62
+ error_message = response[:envelope][:body][:delete_response][:notification][:message]
63
+ raise RateError, error_message
64
+ end
65
+
66
+
67
+ def success?(response)
68
+ response[:envelope][:body][:delete_response] && ( response[:envelope][:body][:delete_response][:notification][:code] == "0" )
69
+ end
70
+
71
+ def headers
72
+ super.merge!('SOAPAction' => 'euExpressRateBook_providerServices_ShipmentHandlingServices_Binder_deleteShipmentRequest')
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ require 'dhl_express_global/label'
2
+ require 'dhl_express_global/request/base'
3
+ require 'dhl_express_global/request/shipment'
4
+ require 'fileutils'
5
+
6
+ module DhlExpressGlobal
7
+ module Request
8
+ class Label < Shipment
9
+
10
+ def initialize(credentials, options = {})
11
+ super(credentials, options)
12
+ @filename = options[:filename]
13
+ end
14
+
15
+ private
16
+
17
+ def success_response(response)
18
+ super
19
+
20
+ label_details = response.merge!({
21
+ :format => @label_specification[:image_type],
22
+ :file_name => @filename
23
+ })
24
+
25
+ DhlExpressGlobal::Label.new label_details
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,114 @@
1
+ require 'dhl_express_global/request/base'
2
+
3
+ module DhlExpressGlobal
4
+ module Request
5
+ class Rate < Base
6
+
7
+ def initialize(credentials, options={})
8
+ super
9
+ end
10
+
11
+ def process_request
12
+ api_response = self.class.post api_url, :body => build_xml, :headers => headers
13
+ puts api_response if @debug
14
+ response = parse_response(api_response)
15
+ if success?(response)
16
+ success_response(response)
17
+ else
18
+ failure_response(response)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_xml
25
+ builder = Nokogiri::XML::Builder.new do |xml|
26
+ xml[:soapenv].Envelope( 'xmlns:soapenv' => "http://schemas.xmlsoap.org/soap/envelope/",
27
+ 'xmlns:ship' => "http://scxgxtt.phx-dc.dhl.com/euExpressRateBook/RateMsgRequest") {
28
+ add_ws_authentication_header(xml)
29
+ xml[:soapenv].Body {
30
+ xml.RateRequest {
31
+ xml.parent.namespace = nil
32
+ xml.RequestedShipment {
33
+ xml.DropOffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
34
+ xml.NextBusinessDay @shipping_options[:next_day] ||= "N"
35
+ xml.Ship {
36
+ add_shipper(xml)
37
+ add_recipient(xml)
38
+ }
39
+ add_requested_packages(xml)
40
+ xml.PaymentInfo @payment_info
41
+ xml.Account @credentials.account_number
42
+ }
43
+ }
44
+ }
45
+ }
46
+ end
47
+ builder.doc.root.to_xml
48
+ end
49
+
50
+ def add_shipper(xml)
51
+ xml.Shipper {
52
+ add_address_street_lines(xml, @shipper[:address])
53
+ xml.City @shipper[:city]
54
+ xml.PostalCode @shipper[:postal_code]
55
+ xml.StateOrProvinceCode @shipper[:state] if @shipper[:state]
56
+ xml.CountryCode @shipper[:country_code]
57
+ }
58
+ end
59
+
60
+ def add_recipient(xml)
61
+ xml.Recipient {
62
+ add_address_street_lines(xml, @recipient[:address])
63
+ xml.City @recipient[:city]
64
+ xml.PostalCode @recipient[:postal_code]
65
+ xml.StateOrProvinceCode @recipient[:state] if @recipient[:state]
66
+ xml.CountryCode @recipient[:country_code]
67
+ }
68
+ end
69
+
70
+ def headers
71
+ super.merge!("SOAPAction" => "euExpressRateBook_providerServices_ShipmentHandlingServices_Binder_getRateRequest")
72
+ end
73
+ ## <RateRequest>
74
+ ## <ClientDetail></ClientDetail>
75
+ ## <RequestedShipment>
76
+ ## <DropOffType>REQUEST_COURIER</DropOffType>
77
+ ## <Ship>
78
+ ## <Shipper>
79
+ ## <StreetLines>1-16-24, Minami-gyotoku</StreetLines>
80
+ ## <City>Ichikawa-shi, Chiba</City>
81
+ ## <PostalCode>272-0138</PostalCode>
82
+ ## <CountryCode>JP</CountryCode>
83
+ ## </Shipper>
84
+ ## <Recipient>
85
+ ## <StreetLines>63 RENMIN LU, QINGDAO SHI</StreetLines>
86
+ ## <City>QINGDAO SHI</City>
87
+ ## <PostalCode>266033</PostalCode>
88
+ ## <CountryCode>CN</CountryCode>
89
+ ## </Recipient>
90
+ ## </Ship>
91
+ ## <Packages>
92
+ ## <RequestedPackages number="1">
93
+ ## <Weight>
94
+ ## <Value>2.0</Value>
95
+ ## </Weight>
96
+ ## <Dimensions>
97
+ ## <Length>13</Length>
98
+ ## <Width>12</Width>
99
+ ## <Height>9</Height>
100
+ ## </Dimensions>
101
+ ## </RequestedPackages>
102
+ ## </Packages>
103
+ ## <ShipTimestamp>2010-11-26T12:00:00GMT-06:00</ShipTimestamp>
104
+ ## <UnitOfMeasurement>SU</UnitOfMeasurement>
105
+ ## <Content>NON_DOCUMENTS</Content>
106
+ ## <DeclaredValue>0000000200</DeclaredValue>
107
+ ## <DeclaredValueCurrecyCode>USD</DeclaredValueCurrecyCode>
108
+ ## <PaymentInfo>DDP</PaymentInfo>
109
+ ## <Account>000000000</Account>
110
+ ## </RequestedShipment>
111
+ ## </RateRequest>
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,128 @@
1
+ require 'dhl_express_global/request/base'
2
+
3
+ module DhlExpressGlobal
4
+ module Request
5
+ class Shipment < Base
6
+
7
+ def initialize(credentials, options={})
8
+ super
9
+ requires!(options, :service_type, :payment_info, :international_detail)
10
+ @international_detail = options[:international_detail]
11
+ requires!(@international_detail, :commodities)
12
+ @commodities = @international_detail[:commodities]
13
+ requires!(@commodities, :description, :customs_value)
14
+ @payment_info = options[:payment_info]
15
+ @label_specification = {
16
+ :image_type => 'PDF',
17
+ :label_template => 'ECOM26_84_001'
18
+ }
19
+
20
+ @label_specification.merge! options[:label_specification] if options[:label_specification]
21
+ end
22
+
23
+ def process_request
24
+ api_response = self.class.post api_url, :body => build_xml, :headers => headers
25
+ puts api_response if @debug
26
+ response = parse_response(api_response)
27
+ if success?(response)
28
+ success_response(response)
29
+ else
30
+ failure_response(response)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_xml
37
+ builder = Nokogiri::XML::Builder.new do |xml|
38
+ xml[:soapenv].Envelope( 'xmlns:soapenv' => "http://schemas.xmlsoap.org/soap/envelope/",
39
+ 'xmlns:ship' => "http://scxgxtt.phx-dc.dhl.com/euExpressRateBook/ShipmentMsgRequest") {
40
+ add_ws_authentication_header(xml)
41
+ xml[:soapenv].Body {
42
+ xml[:ship].ShipmentRequest {
43
+ xml.RequestedShipment {
44
+ xml.parent.namespace = nil
45
+ add_shipment_info(xml)
46
+ xml.ShipTimestamp @shipping_options[:ship_timestamp] ||= (Time.now + 10*60).strftime("%Y-%m-%dT%H:%M:%SGMT%:z")
47
+ xml.PaymentInfo @payment_info
48
+ add_international_detail(xml)
49
+ xml.Ship {
50
+ add_shipper(xml)
51
+ add_recipient(xml)
52
+ }
53
+ add_requested_packages(xml)
54
+ }
55
+ }
56
+ }
57
+ }
58
+ end
59
+ builder.doc.root.to_xml
60
+ end
61
+
62
+ def add_shipment_info(xml)
63
+ xml.ShipmentInfo {
64
+ xml.DropOffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
65
+ xml.ServiceType @service_type
66
+ xml.Account @credentials.account_number
67
+ xml.Currency @shipping_options[:currency].upcase
68
+ xml.UnitOfMeasurement @shipping_options[:unit_of_measurement]
69
+ }
70
+ end
71
+
72
+ def add_international_detail(xml)
73
+ xml.InternationalDetail {
74
+ xml.Commodities {
75
+ xml.NumberOfPieces @commodities[:number_of_pieces] if @commodities[:number_of_pieces]
76
+ xml.Description @commodities[:description]
77
+ xml.CountryOfManufacture @commodities[:country_of_manufacture] if @commodities[:country_of_manufacture]
78
+ xml.Quantity @commodities[:quantity] if @commodities[:quantity]
79
+ xml.UnitPrice @commodities[:unit_price] if @commodities[:unit_price]
80
+ xml.CustomsValue @commodities[:customs_value]
81
+ }
82
+ xml.Content @international_detail[:content] if @international_detail[:content]
83
+ }
84
+ end
85
+
86
+ def add_requested_packages(xml)
87
+ xml.Packages {
88
+ @packages.each_with_index do |package, i|
89
+ xml.RequestedPackages('number' => i + 1) {
90
+ xml.Weight package[:weight][:value]
91
+ xml.Dimensions {
92
+ xml.Length package[:dimensions][:length]
93
+ xml.Width package[:dimensions][:width]
94
+ xml.Height package[:dimensions][:height]
95
+ }
96
+ xml.CustomerReferences @shipping_options[:customer_references] ||= "#{rand(10**10)}"
97
+ }
98
+ end
99
+ }
100
+ end
101
+
102
+
103
+ def failure_response(response)
104
+ error_message = error_response(response)
105
+ raise RateError, error_message
106
+ end
107
+
108
+ def success_response(response)
109
+ @response_details = response[:envelope][:body][:shipment_response]
110
+ end
111
+
112
+ def success?(response)
113
+ response[:envelope][:body][:shipment_response] && !error_response(response)
114
+ end
115
+
116
+ def error_response(response)
117
+ response[:envelope][:body][:shipment_response][:notification][:message]
118
+ rescue
119
+ response[:envelope][:body][:shipment_response][:notification].first[:message]
120
+ end
121
+
122
+ def headers
123
+ super.merge!("SOAPAction" => "euExpressRateBook_providerServices_ShipmentHandlingServices_Binder_createShipmentRequest")
124
+ end
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,32 @@
1
+ require 'dhl_express_global/credentials'
2
+ require 'dhl_express_global/request/shipment'
3
+ require 'dhl_express_global/request/label'
4
+ require 'dhl_express_global/request/delete'
5
+ require 'dhl_express_global/request/rate'
6
+
7
+ module DhlExpressGlobal
8
+
9
+ class Shipment
10
+
11
+ def initialize(options = {})
12
+ @credentials = Credentials.new(options)
13
+ end
14
+
15
+ def label(options = {})
16
+ Request::Label.new(@credentials, options).process_request
17
+ end
18
+
19
+ def delete(options = {})
20
+ Request::Delete.new(@credentials, options).process_request
21
+ end
22
+
23
+ def ship(options = {})
24
+ Request::Shipment.new(@credentials, options).process_request
25
+ end
26
+
27
+ def rate(options = {})
28
+ Request::Rate.new(@credentials, options).process_request
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module DhlExpressGlobal
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ development:
2
+ :username: 'xxx'
3
+ :password: 'xxx'
4
+ :account_number: 'xxx'
5
+ :mode: 'xxx'
6
+
7
+ production:
8
+ :username: 'xxx'
9
+ :password: 'xxx'
10
+ :account_number: 'xxx'
11
+ :mode: 'xxx'
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'date'
3
+
4
+ module DhlExpressGlobal
5
+ describe Shipment do
6
+ let (:dhl) { Shipment.new(dhl_credentials) }
7
+ context '#delete' do
8
+ context 'delete shipment with dispatch confirmation number', :vcr do
9
+ let (:tomorrow) { DateTime.now.next_day.to_date.to_s }
10
+ let(:options) do
11
+ { :pickup_date => tomorrow, :pickup_country => "IT",
12
+ :dispatch_confirmation_number => "FLR-804", :requestor_name => "Requestor", :reason_code => "001" }
13
+ end
14
+
15
+ it 'deletes a shipment' do
16
+ expect{ dhl.delete(options) }.to_not raise_error
17
+ end
18
+ end
19
+ context 'context raise an error when the pickup date is invalid', :vcr do
20
+ let(:options) do
21
+ { :pickup_date => "2014-07-21", :pickup_country => "IT",
22
+ :dispatch_confirmation_number => "FLR-804", :requestor_name => "Requestor", :reason_code => "001" }
23
+ end
24
+
25
+ it 'raises an error' do
26
+ expect { dhl.delete(options) }.to raise_error(DhlExpressGlobal::RateError, "Cancellation of booking was not successful. Requested Pickup was not found.")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+
4
+ module DhlExpressGlobal
5
+ describe Label do
6
+ describe 'ship service for label' do
7
+ let(:dhl) { Shipment.new(dhl_credentials) }
8
+ let(:shipper) do
9
+ { :name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "35 Great Jones St", :city => "New York", :state => "NY", :postal_code => "10012", :country_code => "US" }
10
+ end
11
+ let(:recipient) do
12
+ { :name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Bruehlstrasse, 10", :city => "Ettingen", :state => "CH", :postal_code => "4107", :country_code => "CH" }
13
+ end
14
+ let(:packages) do
15
+ [
16
+ {
17
+ :weight => { :units => "KG", :value => 2.86 },
18
+ :dimensions => { :length => 40, :width => 30, :height => 20, units: "CM" }
19
+ }
20
+ ]
21
+ end
22
+ let(:commodities) do
23
+ { :number_of_pieces => 1, :description => "Clothing", :customs_value => 300.00 }
24
+ end
25
+ let(:international_detail) do
26
+ { :commodities => commodities, :content => "NON_DOCUMENTS" }
27
+ end
28
+ let(:filename) {
29
+ require 'tmpdir'
30
+ File.join(Dir.tmpdir, "label#{rand(15000)}.pdf")
31
+ }
32
+ let(:shipping_options) do
33
+ { :currency => "EUR", :unit_of_measurement => "SI" }
34
+ end
35
+ let(:options) do
36
+ {
37
+ :shipper => shipper,
38
+ :recipient => recipient,
39
+ :packages => packages,
40
+ :payment_info => "DDP",
41
+ :international_detail => international_detail,
42
+ :shipping_options => shipping_options,
43
+ :service_type => "P",
44
+ :filename => filename
45
+ }
46
+ end
47
+
48
+ describe 'label', :vcr do
49
+ before do
50
+ @label = dhl.label(options)
51
+ end
52
+
53
+ it 'should create a label' do
54
+ expect(File).to exist(filename)
55
+ end
56
+
57
+ it 'should return a tracking number' do
58
+ expect(@label).to respond_to('tracking_number')
59
+ end
60
+
61
+ it 'should expose complete response' do
62
+ expect(@label).to respond_to('response_details')
63
+ end
64
+
65
+ it 'should expose the file_name' do
66
+ expect(@label).to respond_to('file_name')
67
+ end
68
+
69
+ after do
70
+ require 'fileutils'
71
+ FileUtils.rm_r(filename) if File.exists?(filename)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'dhl_express_global'
3
+
4
+ describe DhlExpressGlobal::Request::Rate do
5
+ describe 'ship service' do
6
+ let(:dhl) { DhlExpressGlobal::Shipment.new(dhl_credentials) }
7
+ let(:shipper) do
8
+ { :name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "35 Great Jones St", :city => "New York", :state => "NY", :postal_code => "10012", :country_code => "US" }
9
+ end
10
+ let(:recipient) do
11
+ { :name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Bruehlstrasse, 10", :city => "Ettingen", :state => "CH", :postal_code => "4107", :country_code => "CH" }
12
+ end
13
+ let(:packages) do
14
+ [
15
+ {
16
+ :weight => { :units => "KG", :value => 2.86 },
17
+ :dimensions => { :length => 40, :width => 30, :height => 20, units: "CM" }
18
+ }
19
+ ]
20
+ end
21
+
22
+ context "international rate request", :vcr do
23
+ let(:options) do
24
+ {
25
+ shipper: shipper,
26
+ recipient: recipient,
27
+ packages: packages,
28
+ payment_info: "DDP"
29
+ }
30
+ end
31
+
32
+ it "succeeds" do
33
+ expect {
34
+ @rate = dhl.rate(options)
35
+ }.to_not raise_error
36
+
37
+ expect(@rate.class).to_not eq(DhlExpressGlobal::RateError)
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'dhl_express_global'
3
+
4
+ describe DhlExpressGlobal::Request::Shipment do
5
+ describe 'ship service' do
6
+ let(:dhl) { DhlExpressGlobal::Shipment.new(dhl_credentials) }
7
+ let(:shipper) do
8
+ { :name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "35 Great Jones St", :city => "New York", :state => "NY", :postal_code => "10012", :country_code => "US" }
9
+ end
10
+ let(:recipient) do
11
+ { :name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Bruehlstrasse, 10", :city => "Ettingen", :state => "CH", :postal_code => "4107", :country_code => "CH" }
12
+ end
13
+ let(:packages) do
14
+ [
15
+ {
16
+ :weight => { :units => "KG", :value => 2.86 },
17
+ :dimensions => { :length => 40, :width => 30, :height => 20, units: "CM" }
18
+ }
19
+ ]
20
+ end
21
+ let(:commodities) do
22
+ { :number_of_pieces => 1, :description => "Clothing", :customs_value => 300.00 }
23
+ end
24
+ let(:international_detail) do
25
+ { :commodities => commodities, :content => "NON_DOCUMENTS" }
26
+ end
27
+ let(:filename) {
28
+ require 'tmpdir'
29
+ File.join(Dir.tmpdir, "label#{rand(15000)}.pdf")
30
+ }
31
+ let(:shipping_options) do
32
+ { :currency => "EUR", :unit_of_measurement => "SI" }
33
+ end
34
+
35
+ context "international shipment", :vcr do
36
+ let(:options) do
37
+ { :shipper => shipper, :recipient => recipient, :packages => packages, :service_type => "P", :payment_info => "DDP", :international_detail => international_detail, :shipping_options => shipping_options }
38
+ end
39
+
40
+ it "succeeds" do
41
+ expect {
42
+ @shipment = dhl.ship(options)
43
+ }.to_not raise_error
44
+
45
+ expect(@shipment.class).to_not eq(DhlExpressGlobal::RateError)
46
+ end
47
+
48
+ end
49
+
50
+ context "without service_type specified", :vcr do
51
+ let(:options) do
52
+ { :shipper => shipper, :recipient => recipient, :packages => packages, :payment_info => "DDP", :international_detail => international_detail, :shipping_options => shipping_options }
53
+ end
54
+
55
+ it 'raises error' do
56
+ expect {
57
+ @shipment = dhl.ship(options)
58
+ }.to raise_error('Missing Required Parameter service_type')
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ require 'rspec'
2
+ require 'dhl_express_global'
3
+ require 'support/vcr'
4
+ require 'support/credentials'
5
+
6
+ RSpec.configure do |c|
7
+ c.filter_run_excluding :production unless dhl_production_credentials
8
+ c.expect_with :rspec do |expect_config|
9
+ expect_config.syntax = :expect
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ def dhl_credentials
2
+ @dhl_credentials ||= credentials["development"]
3
+ end
4
+
5
+ def dhl_production_credentials
6
+ @dhl_production_credentials ||= credentials["production"]
7
+ end
8
+
9
+ private
10
+
11
+ def credentials
12
+ @credentials ||= YAML.load_file(credentials_path)
13
+ end
14
+
15
+ def credentials_path
16
+ File.expand_path("../../config/dhl_credentials.yml", __FILE__)
17
+ end
@@ -0,0 +1,14 @@
1
+ require 'vcr'
2
+
3
+ VCR.configure do |c|
4
+ c.cassette_library_dir = File.expand_path('../../vcr', __FILE__)
5
+ c.hook_into :webmock
6
+ end
7
+
8
+ RSpec.configure do |c|
9
+ c.include DhlExpressGlobal::Helpers
10
+ c.around(:each, :vcr) do |example|
11
+ name = underscorize(example.metadata[:full_description].split(/\s+/, 2).join("/")).gsub(/[^\w\/]+/, "_")
12
+ VCR.use_cassette(name) { example.call }
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dhl_express_global
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - JONBRWN
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.5.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.5.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: builder
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: xml-simple
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 3.0.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 3.0.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: vcr
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '2.00'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '2.00'
139
+ - !ruby/object:Gem::Dependency
140
+ name: webmock
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: 1.8.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: 1.8.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: debugger
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Ruby wrapper for the DHL Express Global API
182
+ email:
183
+ - jonathanbrown.a@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - .gitignore
189
+ - Gemfile
190
+ - LICENSE.txt
191
+ - README.md
192
+ - Rakefile
193
+ - dhl_express_global.gemspec
194
+ - lib/dhl_express_global.rb
195
+ - lib/dhl_express_global/credentials.rb
196
+ - lib/dhl_express_global/helpers.rb
197
+ - lib/dhl_express_global/label.rb
198
+ - lib/dhl_express_global/request/base.rb
199
+ - lib/dhl_express_global/request/delete.rb
200
+ - lib/dhl_express_global/request/label.rb
201
+ - lib/dhl_express_global/request/rate.rb
202
+ - lib/dhl_express_global/request/shipment.rb
203
+ - lib/dhl_express_global/shipment.rb
204
+ - lib/dhl_express_global/version.rb
205
+ - spec/config/dhl_credentials.example.yml
206
+ - spec/lib/delete_spec.rb
207
+ - spec/lib/label_spec.rb
208
+ - spec/lib/rate_spec.rb
209
+ - spec/lib/shipment_spec.rb
210
+ - spec/spec_helper.rb
211
+ - spec/support/credentials.rb
212
+ - spec/support/vcr.rb
213
+ homepage: ''
214
+ licenses:
215
+ - MIT
216
+ metadata: {}
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - '>='
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ required_rubygems_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - '>='
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ requirements: []
232
+ rubyforge_project:
233
+ rubygems_version: 2.0.6
234
+ signing_key:
235
+ specification_version: 4
236
+ summary: Ruby wrapper for the DHL Express Global API
237
+ test_files:
238
+ - spec/config/dhl_credentials.example.yml
239
+ - spec/lib/delete_spec.rb
240
+ - spec/lib/label_spec.rb
241
+ - spec/lib/rate_spec.rb
242
+ - spec/lib/shipment_spec.rb
243
+ - spec/spec_helper.rb
244
+ - spec/support/credentials.rb
245
+ - spec/support/vcr.rb