shippinglogic 1.2.3
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.
- data/.document +5 -0
- data/CHANGELOG.rdoc +55 -0
- data/LICENSE +20 -0
- data/README.rdoc +175 -0
- data/Rakefile +37 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/lib/shippinglogic.rb +3 -0
- data/lib/shippinglogic/attributes.rb +121 -0
- data/lib/shippinglogic/error.rb +22 -0
- data/lib/shippinglogic/fedex.rb +84 -0
- data/lib/shippinglogic/fedex/cancel.rb +47 -0
- data/lib/shippinglogic/fedex/enumerations.rb +348 -0
- data/lib/shippinglogic/fedex/error.rb +47 -0
- data/lib/shippinglogic/fedex/rate.rb +229 -0
- data/lib/shippinglogic/fedex/request.rb +134 -0
- data/lib/shippinglogic/fedex/response.rb +72 -0
- data/lib/shippinglogic/fedex/service.rb +11 -0
- data/lib/shippinglogic/fedex/ship.rb +238 -0
- data/lib/shippinglogic/fedex/signature.rb +68 -0
- data/lib/shippinglogic/fedex/track.rb +124 -0
- data/lib/shippinglogic/proxy.rb +23 -0
- data/lib/shippinglogic/service.rb +42 -0
- data/lib/shippinglogic/ups.rb +83 -0
- data/lib/shippinglogic/ups/cancel.rb +52 -0
- data/lib/shippinglogic/ups/enumerations.rb +56 -0
- data/lib/shippinglogic/ups/error.rb +42 -0
- data/lib/shippinglogic/ups/label.rb +50 -0
- data/lib/shippinglogic/ups/rate.rb +228 -0
- data/lib/shippinglogic/ups/request.rb +49 -0
- data/lib/shippinglogic/ups/response.rb +58 -0
- data/lib/shippinglogic/ups/service.rb +11 -0
- data/lib/shippinglogic/ups/ship_accept.rb +53 -0
- data/lib/shippinglogic/ups/ship_confirm.rb +170 -0
- data/lib/shippinglogic/ups/track.rb +118 -0
- data/lib/shippinglogic/validation.rb +32 -0
- data/shippinglogic.gemspec +120 -0
- data/spec/attributes_spec.rb +67 -0
- data/spec/config/fedex_credentials.example.yml +4 -0
- data/spec/config/ups_credentials.example.yml +3 -0
- data/spec/error_spec.rb +43 -0
- data/spec/fedex/cancel_spec.rb +10 -0
- data/spec/fedex/error_spec.rb +26 -0
- data/spec/fedex/rate_spec.rb +87 -0
- data/spec/fedex/request_spec.rb +15 -0
- data/spec/fedex/responses/blank.xml +0 -0
- data/spec/fedex/responses/cancel_not_found.xml +7 -0
- data/spec/fedex/responses/failed_authentication.xml +7 -0
- data/spec/fedex/responses/malformed.xml +8 -0
- data/spec/fedex/responses/rate_defaults.xml +7 -0
- data/spec/fedex/responses/rate_insurance.xml +9 -0
- data/spec/fedex/responses/rate_no_services.xml +7 -0
- data/spec/fedex/responses/rate_non_custom_packaging.xml +7 -0
- data/spec/fedex/responses/ship_defaults.xml +7 -0
- data/spec/fedex/responses/ship_with_no_signature.xml +7 -0
- data/spec/fedex/responses/signature_defaults.xml +7 -0
- data/spec/fedex/responses/track_defaults.xml +7 -0
- data/spec/fedex/responses/unexpected.xml +1 -0
- data/spec/fedex/service_spec.rb +19 -0
- data/spec/fedex/ship_spec.rb +37 -0
- data/spec/fedex/signature_spec.rb +11 -0
- data/spec/fedex/spec_helper.rb +84 -0
- data/spec/fedex/track_spec.rb +37 -0
- data/spec/fedex_spec.rb +16 -0
- data/spec/lib/interceptor.rb +17 -0
- data/spec/proxy_spec.rb +42 -0
- data/spec/service_spec.rb +23 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/ups/responses/blank.xml +0 -0
- data/spec/ups/responses/track_defaults.xml +2 -0
- data/spec/ups/spec_helper.rb +43 -0
- data/spec/ups_spec.rb +16 -0
- data/spec/validation_spec.rb +49 -0
- metadata +163 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require "builder"
|
2
|
+
|
3
|
+
module Shippinglogic
|
4
|
+
class UPS
|
5
|
+
# Methods relating to building and sending a request to UPS's web services.
|
6
|
+
module Request
|
7
|
+
private
|
8
|
+
# Convenience method for sending requests to UPS
|
9
|
+
def request(body)
|
10
|
+
real_class.post(base.url + real_class.path, :body => body)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Convenience method to create a builder object so that our builder options are consistent across
|
14
|
+
# the various services.
|
15
|
+
#
|
16
|
+
# Ex: if I want to change the indent level to 3 it should change for all requests built.
|
17
|
+
def builder
|
18
|
+
b = Builder::XmlMarkup.new(:indent => 2)
|
19
|
+
b.instruct!
|
20
|
+
b
|
21
|
+
end
|
22
|
+
|
23
|
+
# A convenience method for building the authentication block in your XML request
|
24
|
+
def build_authentication(b)
|
25
|
+
b.AccessRequest(:"xml:lang" => "en-US") do
|
26
|
+
b.AccessLicenseNumber base.key
|
27
|
+
b.UserId base.account
|
28
|
+
b.Password base.password
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# A convenience method for building the address block in your XML request
|
33
|
+
def build_address(b, type)
|
34
|
+
address_lines = send("#{type}_streets").to_s.split(/(?:\s*\n\s*)+/m, 3)
|
35
|
+
|
36
|
+
b.Address do
|
37
|
+
b.AddressLine1 address_lines[0] if address_lines[0]
|
38
|
+
b.AddressLine2 address_lines[1] if address_lines[1]
|
39
|
+
b.AddressLine3 address_lines[2] if address_lines[2]
|
40
|
+
b.City send("#{type}_city") if send("#{type}_city")
|
41
|
+
b.StateProvinceCode send("#{type}_state") if send("#{type}_state")
|
42
|
+
b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
|
43
|
+
b.CountryCode send("#{type}_country") if send("#{type}_country")
|
44
|
+
b.ResidentialAddressIndicator attribute_names.include?("#{type}_residential") && send("#{type}_residential")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "shippinglogic/ups/error"
|
2
|
+
|
3
|
+
module Shippinglogic
|
4
|
+
class UPS
|
5
|
+
# Methods relating to receiving a response from UPS and cleaning it up.
|
6
|
+
module Response
|
7
|
+
SUCCESSFUL_SEVERITIES = ["Warning"]
|
8
|
+
|
9
|
+
private
|
10
|
+
# Overwriting the request method to clean the response and handle errors.
|
11
|
+
def request(body)
|
12
|
+
response = clean_response(super)
|
13
|
+
|
14
|
+
if success?(response)
|
15
|
+
response
|
16
|
+
else
|
17
|
+
raise Error.new(body, response)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Was the response a success?
|
22
|
+
def success?(response)
|
23
|
+
response.is_a?(Hash) && response[:response][:response_status_code] == "1"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Cleans the response and returns it in a more 'user friendly' format that is easier
|
27
|
+
# to work with.
|
28
|
+
def clean_response(response)
|
29
|
+
cut_to_the_chase(sanitize_response_keys(response))
|
30
|
+
end
|
31
|
+
|
32
|
+
# UPS likes nested XML tags, because they send quite a bit of them back in responses.
|
33
|
+
# This method just 'cuts to the chase' and get to the heart of the response.
|
34
|
+
def cut_to_the_chase(response)
|
35
|
+
response.values.first
|
36
|
+
end
|
37
|
+
|
38
|
+
# Recursively sanitizes the response object by clenaing up any hash keys.
|
39
|
+
def sanitize_response_keys(response)
|
40
|
+
if response.is_a?(Hash)
|
41
|
+
response.inject({}) do |r, (key, value)|
|
42
|
+
r[sanitize_response_key(key)] = sanitize_response_keys(value)
|
43
|
+
r
|
44
|
+
end
|
45
|
+
elsif response.is_a?(Array)
|
46
|
+
response.collect { |r| sanitize_response_keys(r) }
|
47
|
+
else
|
48
|
+
response
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Underscores and symbolizes incoming UPS response keys.
|
53
|
+
def sanitize_response_key(key)
|
54
|
+
key.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
module Shippinglogic
|
4
|
+
class UPS
|
5
|
+
class ShipAccept < Service
|
6
|
+
def self.path
|
7
|
+
"/ShipAccept"
|
8
|
+
end
|
9
|
+
|
10
|
+
class Details
|
11
|
+
class Shipment; attr_accessor :tracking_number, :label; end
|
12
|
+
|
13
|
+
attr_accessor :rate, :currency, :shipments
|
14
|
+
|
15
|
+
def initialize(response)
|
16
|
+
details = response[:shipment_results]
|
17
|
+
|
18
|
+
charges = details[:shipment_charges][:total_charges]
|
19
|
+
self.rate = BigDecimal.new(charges[:monetary_value])
|
20
|
+
self.currency = charges[:currency_code]
|
21
|
+
|
22
|
+
self.shipments = [*details[:package_results]].collect do |package|
|
23
|
+
shipment = Shipment.new
|
24
|
+
shipment.tracking_number = package[:tracking_number]
|
25
|
+
shipment.label = Base64.decode64(package[:label_image][:graphic_image])
|
26
|
+
shipment
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attribute :digest, :string
|
32
|
+
|
33
|
+
private
|
34
|
+
def target
|
35
|
+
@target ||= Details.new(request(build_request))
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_request
|
39
|
+
b = builder
|
40
|
+
build_authentication(b)
|
41
|
+
b.instruct!
|
42
|
+
|
43
|
+
b.ShipmentAcceptRequest do
|
44
|
+
b.Request do
|
45
|
+
b.RequestAction "ShipAccept"
|
46
|
+
end
|
47
|
+
|
48
|
+
b.ShipmentDigest digest
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Shippinglogic
|
2
|
+
class UPS
|
3
|
+
class ShipConfirm < Service
|
4
|
+
def self.path
|
5
|
+
"/ShipConfirm"
|
6
|
+
end
|
7
|
+
|
8
|
+
class Details
|
9
|
+
attr_accessor :digest, :tracking_number, :rate, :currency
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
self.digest = response[:shipment_digest]
|
13
|
+
self.tracking_number = response[:shipment_identification_number]
|
14
|
+
|
15
|
+
charges = response[:shipment_charges][:total_charges]
|
16
|
+
self.rate = BigDecimal.new(charges[:monetary_value])
|
17
|
+
self.currency = charges[:currency_code]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# shipper options
|
22
|
+
attribute :shipper_name, :string
|
23
|
+
attribute :shipper_phone_number, :string
|
24
|
+
attribute :shipper_email, :string
|
25
|
+
attribute :shipper_streets, :string
|
26
|
+
attribute :shipper_city, :string
|
27
|
+
attribute :shipper_state, :string
|
28
|
+
attribute :shipper_postal_code, :string
|
29
|
+
attribute :shipper_country, :string
|
30
|
+
|
31
|
+
# recipient options
|
32
|
+
attribute :recipient_name, :string
|
33
|
+
attribute :recipient_phone_number, :string
|
34
|
+
attribute :recipient_email, :string
|
35
|
+
attribute :recipient_streets, :string
|
36
|
+
attribute :recipient_city, :string
|
37
|
+
attribute :recipient_state, :string
|
38
|
+
attribute :recipient_postal_code, :string
|
39
|
+
attribute :recipient_country, :string
|
40
|
+
attribute :recipient_residential, :boolean, :default => false
|
41
|
+
|
42
|
+
# label options
|
43
|
+
attribute :label_format, :string, :default => "GIF"
|
44
|
+
attribute :label_file_type, :string, :default => "GIF"
|
45
|
+
|
46
|
+
# packaging options
|
47
|
+
attribute :packaging_type, :string, :default => "00"
|
48
|
+
attribute :package_count, :integer, :default => 1
|
49
|
+
attribute :package_weight, :float
|
50
|
+
attribute :package_weight_units, :string, :default => "LBS"
|
51
|
+
attribute :package_length, :integer
|
52
|
+
attribute :package_width, :integer
|
53
|
+
attribute :package_height, :integer
|
54
|
+
attribute :package_dimension_units, :string, :default => "IN"
|
55
|
+
|
56
|
+
# monetary options
|
57
|
+
attribute :currency_type, :string
|
58
|
+
attribute :insured_value, :decimal
|
59
|
+
attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.number }
|
60
|
+
|
61
|
+
# delivery options
|
62
|
+
attribute :service_type, :string
|
63
|
+
#FIXME Setting the signature option to true raises and error. I believe this has something
|
64
|
+
# to do with UPS account-specific settings and signature service availability.
|
65
|
+
attribute :signature, :boolean, :default => false
|
66
|
+
attribute :saturday, :boolean, :default => false
|
67
|
+
|
68
|
+
private
|
69
|
+
def target
|
70
|
+
@target ||= Details.new(request(build_request))
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_request
|
74
|
+
b = builder
|
75
|
+
build_authentication(b)
|
76
|
+
b.instruct!
|
77
|
+
|
78
|
+
b.ShipmentConfirmRequest do
|
79
|
+
b.Request do
|
80
|
+
b.RequestAction "ShipConfirm"
|
81
|
+
b.RequestOption "validate"
|
82
|
+
end
|
83
|
+
|
84
|
+
b.Shipment do
|
85
|
+
b.Shipper do
|
86
|
+
b.Name shipper_name
|
87
|
+
b.ShipperNumber payor_account_number
|
88
|
+
b.PhoneNumber shipper_phone_number
|
89
|
+
b.EMailAddress shipper_email
|
90
|
+
build_address(b, :shipper)
|
91
|
+
end
|
92
|
+
|
93
|
+
b.ShipTo do
|
94
|
+
b.CompanyName recipient_name
|
95
|
+
b.PhoneNumber recipient_phone_number
|
96
|
+
b.EMailAddress recipient_email
|
97
|
+
build_address(b, :recipient)
|
98
|
+
end
|
99
|
+
|
100
|
+
b.PaymentInformation do
|
101
|
+
b.Prepaid do
|
102
|
+
b.BillShipper do
|
103
|
+
b.AccountNumber payor_account_number
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
b.Service do
|
109
|
+
b.Code service_type
|
110
|
+
end
|
111
|
+
|
112
|
+
b.ShipmentServiceOptions do
|
113
|
+
b.SaturdayDelivery if saturday
|
114
|
+
if signature
|
115
|
+
b.DeliveryConfirmation do
|
116
|
+
b.DCISType "1"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
package_count.times do |i|
|
122
|
+
b.Package do
|
123
|
+
b.PackagingType do
|
124
|
+
b.Code packaging_type
|
125
|
+
end
|
126
|
+
|
127
|
+
b.Dimensions do
|
128
|
+
b.UnitOfMeasurement do
|
129
|
+
b.Code package_dimension_units
|
130
|
+
end
|
131
|
+
|
132
|
+
b.Length "%.2f" % package_length
|
133
|
+
b.Width "%.2f" % package_width
|
134
|
+
b.Height "%.2f" % package_height
|
135
|
+
end
|
136
|
+
|
137
|
+
b.PackageWeight do
|
138
|
+
b.UnitOfMeasurement do
|
139
|
+
b.Code package_weight_units
|
140
|
+
end
|
141
|
+
|
142
|
+
b.Weight "%.1f" % package_weight
|
143
|
+
end
|
144
|
+
|
145
|
+
b.PackageServiceOptions do
|
146
|
+
if insured_value
|
147
|
+
b.InsuredValue do
|
148
|
+
b.MonetaryValue insured_value
|
149
|
+
b.CurrencyCode currency_type
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
b.LabelSpecification do
|
158
|
+
b.LabelPrintMethod do
|
159
|
+
b.Code label_file_type
|
160
|
+
end
|
161
|
+
|
162
|
+
b.LabelImageFormat do
|
163
|
+
b.Code label_format
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Shippinglogic
|
2
|
+
class UPS
|
3
|
+
# An interface to the track services provided by UPS. Allows you to get an array of events for a specific
|
4
|
+
# tracking number.
|
5
|
+
#
|
6
|
+
# == Accessor methods / options
|
7
|
+
#
|
8
|
+
# * <tt>tracking_number</tt> - the tracking number
|
9
|
+
#
|
10
|
+
# === Simple Example
|
11
|
+
#
|
12
|
+
# Here is a very simple example:
|
13
|
+
#
|
14
|
+
# ups = Shippinglogic::UPS.new(key, password, account)
|
15
|
+
# tracking_details = ups.track(:tracking_number => "my number")
|
16
|
+
#
|
17
|
+
# tracking_details.status
|
18
|
+
# # => "Delivered"
|
19
|
+
#
|
20
|
+
# tracking_details.signature_name
|
21
|
+
# # => "KKING"
|
22
|
+
#
|
23
|
+
# tracking_details.events.first
|
24
|
+
# # => #<Shippinglogic::UPS::Track::Event @postal_code="95817", @name="Delivered", @state="CA",
|
25
|
+
# # @city="Sacramento", @type="Delivered", @country="US", @occured_at=Mon Dec 08 10:43:37 -0500 2008>
|
26
|
+
#
|
27
|
+
# tracking_details.events.first.name
|
28
|
+
# # => "Delivered"
|
29
|
+
#
|
30
|
+
# === Note
|
31
|
+
#
|
32
|
+
# UPS does support locating packages through means other than a tracking number.
|
33
|
+
# These are not supported and probably won't be until someone needs them. It should
|
34
|
+
# be fairly simple to add, but I could not think of a reason why anyone would want to track
|
35
|
+
# a package with anything other than a tracking number.
|
36
|
+
class Track < Service
|
37
|
+
def self.path
|
38
|
+
"/Track"
|
39
|
+
end
|
40
|
+
|
41
|
+
class Details
|
42
|
+
# Each tracking result is an object of this class
|
43
|
+
class Event; attr_accessor :name, :type, :occured_at, :city, :state, :postal_code, :country; end
|
44
|
+
|
45
|
+
attr_accessor :origin_city, :origin_state, :origin_country,
|
46
|
+
:destination_city, :destination_state, :destination_country,
|
47
|
+
:signature_name, :service_type, :status, :delivery_at,
|
48
|
+
:events
|
49
|
+
|
50
|
+
def initialize(response)
|
51
|
+
details = response[:shipment]
|
52
|
+
|
53
|
+
if origin = details.fetch(:shipper, {})[:address]
|
54
|
+
self.origin_city = origin[:city]
|
55
|
+
self.origin_state = origin[:state_province_code]
|
56
|
+
self.origin_country = origin[:country_code]
|
57
|
+
end
|
58
|
+
|
59
|
+
if destination = details.fetch(:ship_to, {})[:address]
|
60
|
+
self.destination_city = destination[:city]
|
61
|
+
self.destination_state = destination[:state_province_code]
|
62
|
+
self.destination_country = destination[:country_code]
|
63
|
+
end
|
64
|
+
|
65
|
+
package = details[:package]
|
66
|
+
events = package[:activity].is_a?(Array) ? package[:activity] : [package[:activitiy]].compact
|
67
|
+
last_event = events.first
|
68
|
+
delivery = events.detect{|e| e[:status][:status_type][:code] == "D" }
|
69
|
+
|
70
|
+
self.signature_name = last_event && last_event[:signed_for_by_name]
|
71
|
+
self.service_type = details[:service][:description]
|
72
|
+
self.status = last_event && last_event[:status][:status_type][:description]
|
73
|
+
self.delivery_at = delivery && Time.parse(delivery[:date] + delivery[:time])
|
74
|
+
|
75
|
+
self.events = events.collect do |details|
|
76
|
+
event = Event.new
|
77
|
+
status = details[:status][:status_type]
|
78
|
+
event.name = status[:description]
|
79
|
+
event.type = status[:code]
|
80
|
+
#FIXME The proper spelling is "occurred", not "occured."
|
81
|
+
event.occured_at = Time.parse(details[:date] + details[:time])
|
82
|
+
location = details[:activity_location][:address]
|
83
|
+
event.city = location[:city]
|
84
|
+
event.state = location[:state_province_code]
|
85
|
+
event.postal_code = location[:postal_code]
|
86
|
+
event.country = location[:country_code]
|
87
|
+
event
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
attribute :tracking_number, :string
|
93
|
+
|
94
|
+
private
|
95
|
+
# The parent class Service requires that we define this method. This is our kicker. This method is only
|
96
|
+
# called when we need to deal with information from UPS. Notice the caching into the @target variable.
|
97
|
+
def target
|
98
|
+
@target ||= Details.new(request(build_request))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Just building some XML to send off to UPS. UPS requires this particualar format.
|
102
|
+
def build_request
|
103
|
+
b = builder
|
104
|
+
build_authentication(b)
|
105
|
+
b.instruct!
|
106
|
+
|
107
|
+
b.TrackRequest do
|
108
|
+
b.Request do
|
109
|
+
b.RequestAction "Track"
|
110
|
+
b.RequestOption "activity"
|
111
|
+
end
|
112
|
+
|
113
|
+
b.TrackingNumber tracking_number
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|