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.
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