usps 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|