usps 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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +56 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/usps.rb +45 -0
- data/lib/usps/address.rb +72 -0
- data/lib/usps/client.rb +56 -0
- data/lib/usps/configuration.rb +16 -0
- data/lib/usps/errors.rb +46 -0
- data/lib/usps/request.rb +13 -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_lookup.rb +28 -0
- data/lib/usps/request/zip_code_lookup.rb +45 -0
- data/lib/usps/response.rb +7 -0
- data/lib/usps/response/address_standardization.rb +38 -0
- data/lib/usps/response/base.rb +13 -0
- data/lib/usps/response/city_and_state_lookup.rb +28 -0
- data/lib/usps/response/delivery_confirmation.rb +25 -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/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_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/signature_confirmation_spec.rb +0 -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/signature_confirmation_spec.rb +0 -0
- data/spec/response/tracking_lookup_spec.rb +27 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/usps_spec.rb +8 -0
- data/usps.gemspec +129 -0
- metadata +178 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
attr_reader :api, :tag, :secure, :response
|
5
|
+
|
6
|
+
alias :secure? :secure
|
7
|
+
|
8
|
+
# Config given
|
9
|
+
# api: The USPS API name as given in the request URL
|
10
|
+
# tag: The root tag used for the request
|
11
|
+
# secure: Whether or not the request is against the secure server
|
12
|
+
# response: The USPS::Response class used to handle responses
|
13
|
+
def config(options = {})
|
14
|
+
@api = options[:api].to_s
|
15
|
+
@tag = options[:tag].to_s
|
16
|
+
@secure = !!options[:secure]
|
17
|
+
@response = options[:response]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def send!
|
22
|
+
USPS.client.request(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def secure?
|
26
|
+
!!self.class.secure?
|
27
|
+
end
|
28
|
+
|
29
|
+
def api
|
30
|
+
self.class.api
|
31
|
+
end
|
32
|
+
|
33
|
+
def response_for(xml)
|
34
|
+
self.class.response.parse(xml)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build(&block)
|
38
|
+
builder = Builder::XmlMarkup.new(:indent => 0)
|
39
|
+
builder.tag!(self.class.tag, :USERID => USPS.config.username, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -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::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,7 @@
|
|
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
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
private
|
26
|
+
def parse(node)
|
27
|
+
USPS::Address.new(
|
28
|
+
:company => node.search('FirmName').text,
|
29
|
+
:address1 => node.search('Address2').text,
|
30
|
+
:address2 => node.search('Address1').text,
|
31
|
+
:city => node.search('City').text,
|
32
|
+
:state => node.search('State').text,
|
33
|
+
:zip5 => node.search('Zip5').text,
|
34
|
+
:zip4 => node.search('Zip4').text
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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 an address representing the standardized version of the given
|
21
|
+
# address from the results of the query.
|
22
|
+
def get(zip)
|
23
|
+
@data[zip.to_i]
|
24
|
+
end
|
25
|
+
alias :[] :get
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -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,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 'test/unit'
|
2
|
+
require 'rubygems'
|
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
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class USPS::Test
|
2
|
+
module AddressVerification
|
3
|
+
def test_address_standardization_1
|
4
|
+
address = USPS::Address.new(
|
5
|
+
:address => '6406 Ivy Lane',
|
6
|
+
:city => 'Greenbelt',
|
7
|
+
:state => 'MD'
|
8
|
+
).standardize!
|
9
|
+
|
10
|
+
assert_equal '6406 IVY LN', address.address
|
11
|
+
assert_equal 'GREENBELT', address.city
|
12
|
+
assert_equal 'MD', address.state
|
13
|
+
assert_equal '20770', address.zip5
|
14
|
+
assert_equal '1440', address.zip4
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_address_standardization_2
|
18
|
+
address = USPS::Address.new(
|
19
|
+
:address => '8 Wildwood Drive',
|
20
|
+
:city => 'Old Lyme',
|
21
|
+
:state => 'CT'
|
22
|
+
).standardize!
|
23
|
+
|
24
|
+
assert_equal '8 WILDWOOD DR', address.address
|
25
|
+
assert_equal 'OLD LYME', address.city
|
26
|
+
assert_equal 'CT', address.state
|
27
|
+
assert_equal '06371', address.zip5
|
28
|
+
assert_equal '1844', address.zip4
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|