usps 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +56 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/lib/usps.rb +45 -0
  8. data/lib/usps/address.rb +72 -0
  9. data/lib/usps/client.rb +56 -0
  10. data/lib/usps/configuration.rb +16 -0
  11. data/lib/usps/errors.rb +46 -0
  12. data/lib/usps/request.rb +13 -0
  13. data/lib/usps/request/address_standardization.rb +44 -0
  14. data/lib/usps/request/base.rb +42 -0
  15. data/lib/usps/request/city_and_state_lookup.rb +32 -0
  16. data/lib/usps/request/delivery_confirmation.rb +88 -0
  17. data/lib/usps/request/delivery_confirmation_certify.rb +10 -0
  18. data/lib/usps/request/tracking_lookup.rb +28 -0
  19. data/lib/usps/request/zip_code_lookup.rb +45 -0
  20. data/lib/usps/response.rb +7 -0
  21. data/lib/usps/response/address_standardization.rb +38 -0
  22. data/lib/usps/response/base.rb +13 -0
  23. data/lib/usps/response/city_and_state_lookup.rb +28 -0
  24. data/lib/usps/response/delivery_confirmation.rb +25 -0
  25. data/lib/usps/response/tracking_lookup.rb +19 -0
  26. data/lib/usps/test.rb +34 -0
  27. data/lib/usps/test/address_verification.rb +31 -0
  28. data/lib/usps/test/city_and_state_lookup.rb +19 -0
  29. data/lib/usps/test/delivery_confirmation.rb +59 -0
  30. data/lib/usps/test/tracking_lookup.rb +29 -0
  31. data/lib/usps/test/zip_code_lookup.rb +37 -0
  32. data/spec/address_spec.rb +65 -0
  33. data/spec/configuration_spec.rb +19 -0
  34. data/spec/data/address_standardization_1.xml +1 -0
  35. data/spec/data/address_standardization_2.xml +1 -0
  36. data/spec/data/city_and_state_lookup_1.xml +1 -0
  37. data/spec/data/city_and_state_lookup_2.xml +1 -0
  38. data/spec/data/delivery_confirmation_1.xml +1 -0
  39. data/spec/data/delivery_confirmation_2.xml +1 -0
  40. data/spec/data/tracking_lookup_1.xml +1 -0
  41. data/spec/data/tracking_lookup_2.xml +1 -0
  42. data/spec/request/address_standardization_spec.rb +45 -0
  43. data/spec/request/base_spec.rb +16 -0
  44. data/spec/request/city_and_state_lookup_spec.rb +32 -0
  45. data/spec/request/delivery_confirmation_certify_spec.rb +11 -0
  46. data/spec/request/delivery_confirmation_spec.rb +57 -0
  47. data/spec/request/signature_confirmation_spec.rb +0 -0
  48. data/spec/request/tracking_spec.rb +21 -0
  49. data/spec/request/zip_code_lookup_spec.rb +42 -0
  50. data/spec/response/address_standardization_spec.rb +54 -0
  51. data/spec/response/base_spec.rb +0 -0
  52. data/spec/response/city_and_state_lookup_spec.rb +27 -0
  53. data/spec/response/delivery_confirmation_spec.rb +43 -0
  54. data/spec/response/signature_confirmation_spec.rb +0 -0
  55. data/spec/response/tracking_lookup_spec.rb +27 -0
  56. data/spec/spec.opts +1 -0
  57. data/spec/spec_helper.rb +25 -0
  58. data/spec/usps_spec.rb +8 -0
  59. data/usps.gemspec +129 -0
  60. 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,13 @@
1
+ module USPS::Response
2
+ class Base
3
+ attr_accessor :raw
4
+
5
+ class << self
6
+ def parse(xml)
7
+ response = self.new(xml)
8
+ response.raw = xml
9
+ response
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -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