usps-fork 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +89 -0
- data/Rakefile +15 -0
- data/lib/usps.rb +47 -0
- data/lib/usps/address.rb +72 -0
- data/lib/usps/client.rb +55 -0
- data/lib/usps/configuration.rb +17 -0
- data/lib/usps/errors.rb +46 -0
- data/lib/usps/request.rb +14 -0
- data/lib/usps/request/address_standardization.rb +44 -0
- data/lib/usps/request/base.rb +42 -0
- data/lib/usps/request/city_and_state_lookup.rb +32 -0
- data/lib/usps/request/delivery_confirmation.rb +88 -0
- data/lib/usps/request/delivery_confirmation_certify.rb +10 -0
- data/lib/usps/request/tracking_field_lookup.rb +28 -0
- data/lib/usps/request/tracking_lookup.rb +28 -0
- data/lib/usps/request/zip_code_lookup.rb +45 -0
- data/lib/usps/response.rb +8 -0
- data/lib/usps/response/address_standardization.rb +52 -0
- data/lib/usps/response/base.rb +13 -0
- data/lib/usps/response/city_and_state_lookup.rb +42 -0
- data/lib/usps/response/delivery_confirmation.rb +25 -0
- data/lib/usps/response/tracking_field_lookup.rb +34 -0
- data/lib/usps/response/tracking_lookup.rb +19 -0
- data/lib/usps/test.rb +34 -0
- data/lib/usps/test/address_verification.rb +31 -0
- data/lib/usps/test/city_and_state_lookup.rb +19 -0
- data/lib/usps/test/delivery_confirmation.rb +59 -0
- data/lib/usps/test/tracking_lookup.rb +29 -0
- data/lib/usps/test/zip_code_lookup.rb +37 -0
- data/lib/usps/track_detail.rb +49 -0
- data/lib/usps/version.rb +3 -0
- data/spec/address_spec.rb +65 -0
- data/spec/configuration_spec.rb +19 -0
- data/spec/data/address_standardization_1.xml +1 -0
- data/spec/data/address_standardization_2.xml +1 -0
- data/spec/data/city_and_state_lookup_1.xml +1 -0
- data/spec/data/city_and_state_lookup_2.xml +1 -0
- data/spec/data/delivery_confirmation_1.xml +1 -0
- data/spec/data/delivery_confirmation_2.xml +1 -0
- data/spec/data/tracking_field_lookup.xml +41 -0
- data/spec/data/tracking_field_lookup_2.xml +17 -0
- data/spec/data/tracking_lookup_1.xml +1 -0
- data/spec/data/tracking_lookup_2.xml +1 -0
- data/spec/request/address_standardization_spec.rb +45 -0
- data/spec/request/base_spec.rb +16 -0
- data/spec/request/city_and_state_lookup_spec.rb +32 -0
- data/spec/request/delivery_confirmation_certify_spec.rb +11 -0
- data/spec/request/delivery_confirmation_spec.rb +57 -0
- data/spec/request/tracking_field_spec.rb +20 -0
- data/spec/request/tracking_spec.rb +21 -0
- data/spec/request/zip_code_lookup_spec.rb +42 -0
- data/spec/response/address_standardization_spec.rb +54 -0
- data/spec/response/base_spec.rb +0 -0
- data/spec/response/city_and_state_lookup_spec.rb +27 -0
- data/spec/response/delivery_confirmation_spec.rb +43 -0
- data/spec/response/tracking_field_lookup_spec.rb +29 -0
- data/spec/response/tracking_lookup_spec.rb +27 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/track_detail_spec.rb +51 -0
- data/spec/usps_spec.rb +8 -0
- metadata +147 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
# All the address information APIs are essentially identical
|
3
|
+
class CityAndStateLookup < Base
|
4
|
+
config(
|
5
|
+
:api => 'CityStateLookup',
|
6
|
+
:tag => 'CityStateLookupRequest',
|
7
|
+
:secure => false,
|
8
|
+
:response => USPS::Response::CityAndStateLookup
|
9
|
+
)
|
10
|
+
|
11
|
+
# Given a list of zip codes, looks up what city and state they are associated with.
|
12
|
+
#
|
13
|
+
# The USPS api is only capable of handling at most 5 zip codes per request.
|
14
|
+
def initialize(*zip_codes)
|
15
|
+
@zip_codes = zip_codes.flatten
|
16
|
+
|
17
|
+
if(@zip_codes.size > 5)
|
18
|
+
raise ArgumentError, 'at most 5 lookups can be performed per request'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def build
|
23
|
+
super do |builder|
|
24
|
+
@zip_codes.each_with_index do |zip, i|
|
25
|
+
builder.tag!('ZipCode', :ID => i) do
|
26
|
+
builder.tag!('Zip5', zip)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
class DeliveryConfirmation < Base
|
3
|
+
config(
|
4
|
+
:api => 'DeliveryConfirmationV3',
|
5
|
+
:tag => 'DeliveryConfirmationV3.0Request',
|
6
|
+
:secure => true,
|
7
|
+
:response => USPS::Response::DeliveryConfirmation
|
8
|
+
)
|
9
|
+
|
10
|
+
attr_reader :to, :from, :weight, :options, :format
|
11
|
+
|
12
|
+
DEFAULTS = {
|
13
|
+
:type => 1,
|
14
|
+
:format => 'TIF',
|
15
|
+
:service => 'Priority'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# List of valid options and their mapping to their tag
|
19
|
+
OPTIONS = {
|
20
|
+
:type => 'Option',
|
21
|
+
:service => 'ServiceType',
|
22
|
+
:po_zip_code => 'PoZipCode',
|
23
|
+
:label_date => 'LabelDate',
|
24
|
+
:customer_reference => 'CustomerRefNo',
|
25
|
+
:separate_receipt => 'SeparateReceiptPage',
|
26
|
+
:address_service => 'AddressServiceRequested',
|
27
|
+
:sender_name => 'SenderName',
|
28
|
+
:sender_email => 'SenderEMail',
|
29
|
+
:recipient_name => 'RecipientName',
|
30
|
+
:recipient_email => 'RecipientEMail'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
FORMATS = %w(TIF PDF).freeze
|
34
|
+
|
35
|
+
# === Options:
|
36
|
+
# * <tt>:
|
37
|
+
def initialize(to, from, weight, options = {})
|
38
|
+
@to = to
|
39
|
+
@from = from
|
40
|
+
@weight = weight
|
41
|
+
@options = DEFAULTS.merge(options)
|
42
|
+
|
43
|
+
@type = @options.delete(:type)
|
44
|
+
self.format = @options.delete(:format)
|
45
|
+
end
|
46
|
+
|
47
|
+
def format=(format)
|
48
|
+
format = format.upcase
|
49
|
+
|
50
|
+
unless(FORMATS.include?(format))
|
51
|
+
raise ArgumentError, "Format must be one of #{FORMATS.join(',')}"
|
52
|
+
end
|
53
|
+
|
54
|
+
@format = format.upcase
|
55
|
+
end
|
56
|
+
|
57
|
+
def build
|
58
|
+
super do |builder|
|
59
|
+
builder.tag!('Option', @type)
|
60
|
+
builder.tag!('ImageParameters')
|
61
|
+
|
62
|
+
[
|
63
|
+
[self.from, 'From'],
|
64
|
+
[self.to, 'To']
|
65
|
+
].each do |address, prefix|
|
66
|
+
builder.tag!("#{prefix}Name", address.name)
|
67
|
+
builder.tag!("#{prefix}Firm", address.company)
|
68
|
+
builder.tag!("#{prefix}Address1", address.extra_address)
|
69
|
+
builder.tag!("#{prefix}Address2", address.address)
|
70
|
+
builder.tag!("#{prefix}City", address.city)
|
71
|
+
builder.tag!("#{prefix}State", address.state)
|
72
|
+
builder.tag!("#{prefix}Zip5", address.zip5)
|
73
|
+
builder.tag!("#{prefix}Zip4", address.zip4)
|
74
|
+
end
|
75
|
+
|
76
|
+
builder.tag!('WeightInOunces', self.weight)
|
77
|
+
|
78
|
+
@options.each_pair do |k,v|
|
79
|
+
OPTIONS[k].tap do |tag|
|
80
|
+
builder.tag!(tag, v.to_s) if tag
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
builder.tag!('ImageType', @format)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
class DeliveryConfirmationCertify < USPS::Request::DeliveryConfirmation
|
3
|
+
config(
|
4
|
+
:api => 'DelivConfirmCertifyV3',
|
5
|
+
:tag => 'DelivConfirmCertifyV3.0Request',
|
6
|
+
:secure => true,
|
7
|
+
:response => USPS::Response::DeliveryConfirmation
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
# Given a valid USPS tracking number, use this class to request
|
3
|
+
# tracking information from USPS's systems.
|
4
|
+
#
|
5
|
+
# Returns a USPS::Response::TrackingFieldLookup object with the pertinent
|
6
|
+
# information
|
7
|
+
class TrackingFieldLookup < Base
|
8
|
+
config(
|
9
|
+
:api => 'TrackV2',
|
10
|
+
:tag => 'TrackFieldRequest',
|
11
|
+
:secure => false,
|
12
|
+
:response => USPS::Response::TrackingFieldLookup
|
13
|
+
)
|
14
|
+
|
15
|
+
# Build a new TrackingLookup request.
|
16
|
+
# Takes the USPS tracking number to request information for
|
17
|
+
def initialize(track_id)
|
18
|
+
@track_id = track_id
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
super do |builder|
|
23
|
+
builder.tag!('TrackID', :ID => @track_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
# Given a valid USPS tracking number, use this class to request
|
3
|
+
# tracking information from USPS's systems.
|
4
|
+
#
|
5
|
+
# Returns a USPS::Response::TrackingLookup object with the pertinent
|
6
|
+
# information
|
7
|
+
class TrackingLookup < Base
|
8
|
+
config(
|
9
|
+
:api => 'TrackV2',
|
10
|
+
:tag => 'TrackRequest',
|
11
|
+
:secure => false,
|
12
|
+
:response => USPS::Response::TrackingLookup
|
13
|
+
)
|
14
|
+
|
15
|
+
# Build a new TrackingLookup request.
|
16
|
+
# Takes the USPS tracking number to request information for
|
17
|
+
def initialize(track_id)
|
18
|
+
@track_id = track_id
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
super do |builder|
|
23
|
+
builder.tag!('TrackID', :ID => @track_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
# This class is essentially identical to AddressStandardization but do to how
|
3
|
+
# #build inheritance is being done it's easier just to reimplement it for now.
|
4
|
+
#
|
5
|
+
# TODO: #send! could be made smarter to send lookup batches
|
6
|
+
class ZipCodeLookup < Base
|
7
|
+
config(
|
8
|
+
:api => 'ZipCodeLookup',
|
9
|
+
:tag => 'ZipCodeLookupRequest',
|
10
|
+
:secure => false,
|
11
|
+
:response => USPS::Response::AddressStandardization
|
12
|
+
)
|
13
|
+
|
14
|
+
# At most 5 zip codes can be retrieved at once
|
15
|
+
def initialize(*addresses)
|
16
|
+
@addresses = addresses
|
17
|
+
|
18
|
+
if @addresses.size > 5
|
19
|
+
raise ArgumentError, 'at most 5 lookups can be performed per request'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def response_for(xml)
|
24
|
+
self.class.response.new(@addresses, xml)
|
25
|
+
end
|
26
|
+
|
27
|
+
def build
|
28
|
+
super do |builder|
|
29
|
+
@addresses.each_with_index do |addy, i|
|
30
|
+
builder.tag!('Address', :ID => i) do
|
31
|
+
builder.tag!('FirmName', addy.firm)
|
32
|
+
|
33
|
+
# Address 1 and 2 are backwards compared to how they appear on an
|
34
|
+
# envelope.
|
35
|
+
builder.tag!('Address1', addy.extra_address)
|
36
|
+
builder.tag!('Address2', addy.address)
|
37
|
+
|
38
|
+
builder.tag!('City', addy.city)
|
39
|
+
builder.tag!('State', addy.state)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module USPS::Response
|
2
|
+
autoload :Base, 'usps/response/base'
|
3
|
+
autoload :CityAndStateLookup, 'usps/response/city_and_state_lookup'
|
4
|
+
autoload :DeliveryConfirmation, 'usps/response/delivery_confirmation'
|
5
|
+
autoload :AddressStandardization, 'usps/response/address_standardization'
|
6
|
+
autoload :TrackingLookup, 'usps/response/tracking_lookup'
|
7
|
+
autoload :TrackingFieldLookup, 'usps/response/tracking_field_lookup'
|
8
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# TODO: AddressStandardization _can_ handle up to 5 addresses at once and each
|
2
|
+
# can be valid or error out. Currently the system raises an exception if any
|
3
|
+
# are invalid. The error should be by address.
|
4
|
+
module USPS::Response
|
5
|
+
class AddressStandardization < Base
|
6
|
+
def initialize(addresses, xml)
|
7
|
+
@addresses = {}
|
8
|
+
|
9
|
+
[addresses].flatten.each_with_index do |addy, i|
|
10
|
+
@addresses[addy] = parse(xml.search("Address[@ID='#{i}']"))
|
11
|
+
|
12
|
+
# Name is not sent nor received so lets make sure to set it so the
|
13
|
+
# standardized version is roughly equivalent
|
14
|
+
@addresses[addy].name = addy.name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an address representing the standardized version of the given
|
19
|
+
# address from the results of the query.
|
20
|
+
def get(address)
|
21
|
+
@addresses[address]
|
22
|
+
end
|
23
|
+
alias :[] :get
|
24
|
+
|
25
|
+
def addresses
|
26
|
+
@addresses
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_h
|
30
|
+
hash = {}
|
31
|
+
@addresses.each_pair do |key, value|
|
32
|
+
hash[key.to_h] = value.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
hash
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def parse(node)
|
40
|
+
USPS::Address.new(
|
41
|
+
:company => node.search('FirmName').text,
|
42
|
+
:address1 => node.search('Address2').text,
|
43
|
+
:address2 => node.search('Address1').text,
|
44
|
+
:city => node.search('City').text,
|
45
|
+
:state => node.search('State').text,
|
46
|
+
:zip5 => node.search('Zip5').text,
|
47
|
+
:zip4 => node.search('Zip4').text,
|
48
|
+
:return_text => node.search('ReturnText').text,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module USPS::Response
|
2
|
+
class CityAndStateLookup < Base
|
3
|
+
# Result record for a city and state lookup.
|
4
|
+
class Result < Struct.new(:zip, :city, :state); end
|
5
|
+
|
6
|
+
def initialize(xml)
|
7
|
+
@data = {}
|
8
|
+
|
9
|
+
xml.search('ZipCode').each do |node|
|
10
|
+
zip = node.search('Zip5').text.to_i
|
11
|
+
|
12
|
+
@data[zip] = Result.new(
|
13
|
+
zip,
|
14
|
+
node.search('City').text,
|
15
|
+
node.search('State').text
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a single city/state pair given a zip5
|
21
|
+
def get(zip)
|
22
|
+
@data[zip.to_i]
|
23
|
+
end
|
24
|
+
alias :[] :get
|
25
|
+
|
26
|
+
# Returns all city/state data from the query results
|
27
|
+
def data
|
28
|
+
@data
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns all city/state data as a pure Ruby hash (e.g. no Structs as values)
|
32
|
+
def to_h
|
33
|
+
hash = {}
|
34
|
+
@data.each_pair do |key, value|
|
35
|
+
hash[key] = value.to_h
|
36
|
+
end
|
37
|
+
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module USPS::Response
|
2
|
+
class DeliveryConfirmation < Base
|
3
|
+
attr_reader :label, :confirmation, :address, :postnet
|
4
|
+
|
5
|
+
alias :confirmation_number :confirmation
|
6
|
+
|
7
|
+
def initialize(xml)
|
8
|
+
# Label is Base64 encoded
|
9
|
+
@label = xml.search('DeliveryConfirmationLabel').text.unpack("m*")[0]
|
10
|
+
@confirmation = xml.search('DeliveryConfirmationNumber').text
|
11
|
+
@postnet = xml.search('Postnet').text
|
12
|
+
|
13
|
+
@address = USPS::Address.new(
|
14
|
+
:name => xml.search('ToName').text,
|
15
|
+
:company => xml.search('ToFirm').text,
|
16
|
+
:address => xml.search('ToAddress2').text,
|
17
|
+
:address2 => xml.search('ToAddress1').text,
|
18
|
+
:city => xml.search('ToCity').text,
|
19
|
+
:state => xml.search('ToState').text,
|
20
|
+
:zip5 => xml.search('ToZip5').text,
|
21
|
+
:zip4 => xml.search('ToZip4').text
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module USPS::Response
|
2
|
+
# Response object from a USPS::Request::TrackingLookup request.
|
3
|
+
# Includes a summary of the current status of the shipment, along with
|
4
|
+
# an array of details of the shipment's progress
|
5
|
+
class TrackingFieldLookup < Base
|
6
|
+
|
7
|
+
attr_accessor :summary, :details
|
8
|
+
|
9
|
+
|
10
|
+
def initialize(xml)
|
11
|
+
@summary = parse(xml.search("TrackSummary"))
|
12
|
+
@details = []
|
13
|
+
xml.search("TrackDetail").each do |detail|
|
14
|
+
@details << parse(detail)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def parse(node)
|
20
|
+
USPS::TrackDetail.new(
|
21
|
+
:event_time => node.search('EventTime').text,
|
22
|
+
:event_date => node.search('EventDate').text,
|
23
|
+
:event => node.search('Event').text,
|
24
|
+
:event_city => node.search('EventCity').text,
|
25
|
+
:event_state => node.search('EventState').text,
|
26
|
+
:event_zip_code => node.search('EventZIPCode').text,
|
27
|
+
:event_country => node.search('EventCountry').text,
|
28
|
+
:firm_name => node.search('FirmName').text,
|
29
|
+
:name => node.search('Name').text,
|
30
|
+
:authorized_agent => node.search('AuthorizedAgent').text
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module USPS::Response
|
2
|
+
# Response object from a USPS::Request::TrackingLookup request.
|
3
|
+
# Includes a summary of the current status of the shipment, along with
|
4
|
+
# an array of details of the shipment's progress
|
5
|
+
class TrackingLookup < Base
|
6
|
+
|
7
|
+
attr_accessor :summary, :details
|
8
|
+
|
9
|
+
def initialize(xml)
|
10
|
+
@summary = xml.search("TrackSummary").text
|
11
|
+
|
12
|
+
@details = []
|
13
|
+
xml.search("TrackDetail").each do |detail|
|
14
|
+
@details << detail.text
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/usps/test.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'usps'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
module USPS
|
5
|
+
# This class is a test runner for the various test requests that are outlined
|
6
|
+
# in the USPS API documentation. These tests are often used to determine if a
|
7
|
+
# developer is allowed to gain access to the production systems.
|
8
|
+
#
|
9
|
+
# Running this test suite should fullfil all requirements for access to the production
|
10
|
+
# system for the APIs supported by the library.
|
11
|
+
class Test < Test::Unit::TestCase
|
12
|
+
require 'usps/test/zip_code_lookup'
|
13
|
+
require 'usps/test/address_verification'
|
14
|
+
require 'usps/test/city_and_state_lookup'
|
15
|
+
require 'usps/test/tracking_lookup'
|
16
|
+
|
17
|
+
if(ENV['USPS_USER'].nil?)
|
18
|
+
raise 'USPS_USER must be set in the environment to run these tests'
|
19
|
+
end
|
20
|
+
|
21
|
+
USPS.configure do |config|
|
22
|
+
# Being explicit even though it's set in the configuration by default
|
23
|
+
config.username = ENV['USPS_USER']
|
24
|
+
|
25
|
+
# Set USPS_LIVE to anything to run against production
|
26
|
+
config.testing = true
|
27
|
+
end
|
28
|
+
|
29
|
+
include ZipCodeLookup
|
30
|
+
include CityAndStateLookup
|
31
|
+
include AddressVerification
|
32
|
+
include TrackingLookup
|
33
|
+
end
|
34
|
+
end
|