awesome_usps 0.5.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.
@@ -0,0 +1,97 @@
1
+ module AwesomeUsps #:nodoc:
2
+ class Location
3
+
4
+ attr_reader :options,
5
+ :country,
6
+ :name,
7
+ :last_name,
8
+ :firm_name,
9
+ :zip5,
10
+ :zip4,
11
+ :state,
12
+ :city,
13
+ :address1,
14
+ :address2,
15
+ :address3,
16
+ :phone,
17
+ :facility_type,
18
+ :from_urbanization
19
+ #:fax,
20
+ #:address_type
21
+
22
+ alias_method :postal_code, :zip5
23
+ alias_method :postal, :postal_code
24
+ alias_method :zip, :postal
25
+ alias_method :province, :state
26
+ alias_method :territory, :province
27
+ alias_method :region, :province
28
+ alias_method :first_name, :name
29
+
30
+ def initialize(options = {})
31
+ @country = options[:country]
32
+ @name = options[:name] || options[:first_name]
33
+ @last_name= options[:last_name]
34
+ @firm_name = options[:firm_name]
35
+ @zip5 = options[:postal_code] || options[:postal] || options[:zip5]
36
+ @zip4 = options[:zip4]
37
+ @state = options[:province] || options[:state]
38
+ @city = options[:city]
39
+ @address1 = options[:address1]
40
+ @address2 = options[:address2]
41
+ @address3=options [:address3]
42
+ @phone = options[:phone]
43
+ @facility_type = options[:facility_type]
44
+ @from_urbanization =options[:from_urbanization]
45
+ end
46
+
47
+ def self.from(object, options={})
48
+ return object if object.is_a? FotoVerite::Location
49
+ attr_mappings = {
50
+ :name => [:name, :first_name],
51
+ #:country => [:country_code, :country],
52
+ :zip5 => [:postal_code, :zip5, :postal],
53
+ :zip4 => [:zip4],
54
+ :state => [ :province, :state],
55
+ :city => [:city],
56
+ :address1 => [:address1],
57
+ :address2 => [:address2],
58
+ :facility_type => [:facility_type]
59
+ }
60
+ attributes = {}
61
+ hash_access = begin
62
+ object[:some_symbol]
63
+ true
64
+ rescue
65
+ false
66
+ end
67
+ attr_mappings.each do |pair|
68
+ pair[1].each do |sym|
69
+ if value = (object[sym] if hash_access) || (object.send(sym) if object.respond_to?(sym) && (!hash_access || !Hash.public_instance_methods.include?(sym.to_s)))
70
+ attributes[pair[0]] = value
71
+ break
72
+ end
73
+ end
74
+ end
75
+ self.new(attributes.update(options))
76
+ end
77
+
78
+
79
+ def to_s
80
+ prettyprint.gsub(/\n/, ' ')
81
+ end
82
+
83
+ def prettyprint
84
+ chunks = []
85
+ chunks << [@name,@firm_name].reject {|e| e.blank?}.join("\n")
86
+ chunks << [@address1,@address2].reject {|e| e.blank?}.join("\n")
87
+ chunks << [@city,@state,@zip5].reject {|e| e.blank?}.join(', ')
88
+ chunks.reject {|e| e.blank?}.join("\n")
89
+ end
90
+
91
+ def inspect
92
+ string = prettyprint
93
+ string
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,127 @@
1
+ module AwesomeUsps
2
+ module OpenDistrubutePriority
3
+
4
+ MAX_RETRIES = 3
5
+
6
+ LIVE_DOMAIN = 'secure.shippingapis.com'
7
+ LIVE_RESOURCE = '/ShippingAPI.dll'
8
+
9
+
10
+ API_CODES = {:open_distrubute_priority => "OpenDistributePriority",
11
+ :open_distribute_priority_certify => "OpenDistributePriorityCertify"}
12
+
13
+ def open_distrubute_priority_label(orgin, destination, package_weight_in_ounces, mail_type, image_type, label_type=1, options={})
14
+ @package_weight_in_ounces = package_weight_in_ounces
15
+ @origin = origin
16
+ @destination = destination
17
+ @mail_type = mail_type
18
+ @image_type = image_type
19
+ @options =options
20
+ @api = "OpenDistributePriorityRequest"
21
+ request = open_distrubute_priority_xml
22
+ #YES THE API IS THAT STUPID THAT WE MUST PASS WHAT TYPE OF MIME TYPE!
23
+ commit_open_distrubute_priority_xml(:open_distrubute_priority, request, image_type, false)
24
+ end
25
+
26
+ def canned_open_distrubute_priority_label_test
27
+ @origin = Location.new( :name=> "John Smith", :address2 => "6406 Ivy Lane", :state => 'MD', :city => 'Greenbelt', :zip5 => '20770')
28
+ @destination =Location.new( :name=> "Fairfax Post Office", :address2 =>"10660 Page Ave", :state => 'VA', :city => 'Fairfax', :zip5 => "22030", :facility_type => "DDU")
29
+ @mail_type = "Letters"
30
+ @image_type = "PDF"
31
+ @package_weight_in_ounces = 1
32
+ @options = {:address_service => true, :permit_number => "21718", :permit_zip => "07204"}
33
+ @api = "OpenDistributePriorityCertifyRequest"
34
+ request= open_distrubute_priority_xml
35
+ commit_open_distrubute_priority_xml(:open_distribute_priority_certify, request, @image_type, true)
36
+ end
37
+
38
+ private
39
+ def open_distrubute_priority_xml
40
+ xm = Builder::XmlMarkup.new
41
+ xm.tag!("#{@api}", "USERID"=>"#{@username}") do
42
+ xm.PermitNumber(@options[:permit_number])
43
+ xm.PermitIssuingPOZip5(@options[:permit_zip])
44
+ xm.FromName(@origin.name)
45
+ xm.FromFirm(@origin.firm_name)
46
+ xm.FromAddress1(@origin.address1) #Used for an apartment or suite number. Yes the API is a bit fucked.
47
+ xm.FromAddress2(@origin.address2)
48
+ xm.FromCity(@origin.city)
49
+ xm.FromState(@origin.state)
50
+ xm.FromZip5(@origin.zip5)
51
+ xm.FromZip4(@origin.zip4)
52
+ xm.POZipCode(@options[:po_zip_code])
53
+ xm.ToFacilityName(@destination.name)
54
+ xm.ToFacilityAddress1(@destination.address1)
55
+ xm.ToFacilityAddress2(@destination.address2)
56
+ xm.ToFacilityCity(@destination.city)
57
+ xm.ToFacilityState(@destination.state)
58
+ xm.ToFacilityZip5(@destination.zip5)
59
+ xm.ToFacilityZip4(@destination.zip4)
60
+ xm.FacilityType(@destination.facility_type)
61
+ xm.MailClassEnclosed(@mail_type)
62
+ xm.MailClassOther(@options[:other])
63
+ xm.WeightInPounds("0")
64
+ xm.WeightInOunces(@package_weight_in_ounces)
65
+ xm.ImageType(@image_type)
66
+ xm.SeparateReceiptPage(@options[:seperate])
67
+ xm.LabelDate(@options[:label_date])
68
+ xm.AllowNonCleansedFacilityAddr("false")
69
+ end
70
+ end
71
+
72
+ def parse_open_distrubute_priority(xml, image_type)
73
+ if image_type == "TIF"
74
+ image_type = "image/tif"
75
+ else
76
+ image_type = "application/pdf"
77
+ end
78
+ parse = Hpricot.parse(xml)/:error
79
+ if parse != []
80
+ RAILS_DEFAULT_LOGGER.info "#{xml}"
81
+ return (Hpricot.parse(xml)/:description).inner_html
82
+ else
83
+ number = Hpricot.parse(xml)/:openDistributeprioritynumber
84
+ label = Hpricot.parse(xml)/:opendistributeprioritylabel
85
+ return {:image_type => image_type, :number => number.inner_html, :label => label.inner_html}
86
+ end
87
+ end
88
+
89
+ def commit_open_distrubute_priority_xml(action, request, image_type, test=false)
90
+ retries = MAX_RETRIES
91
+ begin
92
+ #If and when their testing resource works again this will be useful tertiary command
93
+ url = URI.parse("https://#{LIVE_DOMAIN}#{LIVE_RESOURCE}")
94
+ req = Net::HTTP::Post.new(url.path)
95
+ req.set_form_data({'API' => API_CODES[action], 'XML' => request})
96
+ response = Net::HTTP.new(url.host, 443)
97
+ response.use_ssl
98
+ response.open_timeout = 5
99
+ response.read_timeout = 5
100
+ response.use_ssl = true
101
+ response.start
102
+
103
+ rescue Timeout::Error
104
+ if retries > 0
105
+ retries -= 1
106
+ retry
107
+ else
108
+ RAILS_DEFAULT_LOGGER.warn "The connection to the remote server timed out"
109
+ return "We appoligize for the inconvience but our USPS service is busy at the moment. To retry please refresh the browser"
110
+ end
111
+ rescue SocketError
112
+ RAILS_DEFAULT_LOGGER.error "There is a socket error with USPS plugin"
113
+ return "We appoligize for the inconvience but there is a problem with our server. To retry please refresh the browser"
114
+ end
115
+
116
+ response = response.request(req)
117
+ case response
118
+ when Net::HTTPSuccess
119
+
120
+ parse_open_distrubute_priority(response.body, image_type)
121
+ else
122
+ RAILS_DEFAULT_LOGGER.warn("USPS plugin settings are wrong #{response.body}")
123
+ return "USPS plugin settings are wrong #{response.body}"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,132 @@
1
+ module AwesomeUsps #:nodoc:
2
+ class Package
3
+ GRAMS_IN_AN_OUNCE = 28.3495231
4
+ OUNCES_IN_A_GRAM = 0.0352739619
5
+ INCHES_IN_A_CM = 0.393700787
6
+ CM_IN_AN_INCH = 2.54
7
+
8
+ cattr_accessor :default_options
9
+ attr_reader :options, :value, :currency
10
+
11
+ # Package.new(100, [10, 20, 30], :units => :metric)
12
+ def initialize(grams_or_ounces, dimensions, options = {})
13
+ options = @@default_options.update(options) if @@default_options
14
+ options.symbolize_keys!
15
+ @options = options
16
+
17
+ imperial = options[:units] == :imperial
18
+ dimensions = Array(dimensions)
19
+
20
+ @ounces,@grams = nil
21
+ if grams_or_ounces.nil?
22
+ @grams = @ounces = 0
23
+ elsif imperial
24
+ @ounces = grams_or_ounces
25
+ else
26
+ @grams = grams_or_ounces
27
+ end
28
+
29
+ @inches,@centimetres = nil
30
+ if dimensions.empty?
31
+ @inches = @centimetres = [0,0,0]
32
+ else
33
+ process_dimensions(dimensions,imperial)
34
+ end
35
+
36
+ @value = Package.cents_from(options[:value])
37
+ @currency = options[:currency] || (options[:value].currency if options[:value].respond_to?(:currency))
38
+ @cylinder = (options[:cylinder] || options[:tube]) ? true : false
39
+ end
40
+
41
+ def cylinder?
42
+ @cylinder
43
+ end
44
+ alias_method :tube?, :cylinder?
45
+
46
+ def ounces(options={})
47
+ case options[:type]
48
+ when *[nil,:actual]: @ounces ||= grams(options) * OUNCES_IN_A_GRAM
49
+ when *[:volumetric,:dimensional]: @volumetric_ounces ||= grams(options) * OUNCES_IN_A_GRAM
50
+ when :billable: @billable_ounces ||= [ounces,ounces(:type => :volumetric)].max
51
+ end
52
+ end
53
+ alias_method :oz, :ounces
54
+
55
+ def grams(options={})
56
+ case options[:type]
57
+ when *[nil,:actual]: @grams ||= ounces(options) * GRAMS_IN_AN_OUNCE
58
+ when *[:volumetric,:dimensional]: @volumetric_grams ||= centimetres(:box_volume) / 6.0
59
+ when :billable: [grams,grams(:type => :volumetric)].max
60
+ end
61
+ end
62
+ alias_method :g, :grams
63
+
64
+ def pounds(options={})
65
+ ounces(options) / 16.0
66
+ end
67
+ alias_method :lb, :pounds
68
+ alias_method :lbs, :pounds
69
+
70
+ def kilograms(options={})
71
+ grams(options) / 1000.0
72
+ end
73
+ alias_method :kg, :kilograms
74
+ alias_method :kgs, :kilograms
75
+
76
+ def inches(measurement=nil)
77
+ @inches ||= @centimetres.map {|cm| cm * INCHES_IN_A_CM}
78
+ measurement.nil? ? @inches : measure(measurement, @inches)
79
+ end
80
+ alias_method :in, :inches
81
+
82
+ def centimetres(measurement=nil)
83
+ @centimetres ||= @inches.map {|inches| inches * CM_IN_AN_INCH}
84
+ measurement.nil? ? @centimetres : measure(measurement, @centimetres)
85
+ end
86
+ alias_method :cm, :centimetres
87
+
88
+ def self.cents_from(money)
89
+ return nil if money.nil?
90
+ if money.respond_to?(:cents)
91
+ return money.cents
92
+ else
93
+ return case money
94
+ when Float
95
+ (money * 100).to_i
96
+ when String
97
+ money =~ /\./ ? (money.to_f * 100).to_i : money.to_i
98
+ else
99
+ money.to_i
100
+ end
101
+ end
102
+ end
103
+
104
+
105
+ private
106
+
107
+ def measure(measurement, ary)
108
+ case measurement
109
+ when Fixnum: ary[measurement]
110
+ when *[:x,:max,:length,:long]: ary[2]
111
+ when *[:y,:mid,:width,:wide]: ary[1]
112
+ when *[:z,:min,:height,:depth,:high,:deep]: ary[0]
113
+ when *[:girth,:around,:circumference]
114
+ self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 2) : (2 * ary[0]) + (2 * ary[1])
115
+ when :volume: self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 4)**2 * ary[2] : measure(:box_volume,ary)
116
+ when :box_volume: ary[0] * ary[1] * ary[2]
117
+ end
118
+ end
119
+
120
+ def process_dimensions(dimensions, imperial_units)
121
+ units = imperial_units ? 'inches' : 'centimetres'
122
+ self.instance_variable_set("@#{units}", dimensions.sort)
123
+ units_array = self.instance_variable_get("@#{units}")
124
+ # [1,2] => [1,1,2]
125
+ # [5] => [5,5,5]
126
+ # etc..
127
+ 2.downto(units_array.length) do |n|
128
+ units_array.unshift(units_array[0])
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,143 @@
1
+ module AwesomeUsps
2
+ module ServiceStandard
3
+ MAX_RETRIES = 3
4
+
5
+ LIVE_DOMAIN = 'production.shippingapis.com'
6
+ LIVE_RESOURCE = '/ShippingAPI.dll'
7
+
8
+ TEST_DOMAIN ='testing.shippingapis.com'
9
+ TEST_RESOURCE = '/ShippingAPITest.dll'
10
+
11
+ API_CODES ={
12
+ :priority_mail => 'PriorityMail',
13
+ :standard => 'StandardB',
14
+ :express => 'ExpressMailCommitment'
15
+ }
16
+
17
+ # Takes your package tracking number and returns information for the USPS web API
18
+ def priority_mail_estimated_time(origin, destination)
19
+ @origin = orgin
20
+ @destination=destination
21
+ request = xml_for_estimated_time_for_delivery("PriorityMailRequest")
22
+ commit_service_standard_request(:priority_mail, request ,false)
23
+ end
24
+
25
+ def standard_mail_estimated_time(origin, destination)
26
+ @origin = orgin
27
+ @destination=destination
28
+ request = xml_for_estimated_time_for_delivery("StandardBRequest")
29
+ commit_service_standard_request(:standard, request ,false)
30
+ end
31
+
32
+ def express_mail_commitment(origin, destination, date=nil)
33
+ @origin = orgin
34
+ @destination=destination
35
+ @date = date
36
+ request = xml_for_estimated_time_for_delivery("ExpressMailCommitmentRequest")
37
+ commit_service_standard_request(:express, request ,false)
38
+ end
39
+
40
+ def canned_standard_mail_estimated_time_test
41
+ @origin = Location.new( :zip5 => '4')
42
+ @destination = Location.new( :zip5 => '4')
43
+ request = xml_for_estimated_time_for_delivery("PriorityMailRequest")
44
+ commit_service_standard_request(:priority_mail, request ,true)
45
+ end
46
+
47
+ def canned_priority_mail_estimated_time_test
48
+ @origin = Location.new( :zip5 => '4')
49
+ @destination = Location.new( :zip5 => '4')
50
+ request = xml_for_estimated_time_for_delivery("PriorityMailRequest")
51
+ commit_service_standard_request(:standard, request ,true)
52
+ end
53
+
54
+ def canned_express_mail_commitment_test
55
+ @origin= Location.new( :zip5 =>'20770')
56
+ @destination=Location.new( :zip5 =>'11210')
57
+ @date = '05-Aug-2004'
58
+ request = xml_for_estimated_time_for_delivery("ExpressMailCommitmentRequest")
59
+ commit_service_standard_request(:express, request ,true)
60
+ end
61
+
62
+ # XML from a straight string.
63
+ # "<TrackFieldRequest USERID='#{@username}'><TrackID ID='#{@tracking_number}'></TrackID></TrackFieldRequest>"
64
+ def xml_for_estimated_time_for_delivery(type_of_request)
65
+ xm = Builder::XmlMarkup.new
66
+ xm.tag!(type_of_request, "USERID"=>"#{@username}") do
67
+ xm.OriginZIP(@origin.zip5)
68
+ xm.DestinationZIP(@destination.zip5)
69
+ xm.Date(@date) if type_of_request == "ExpressMailCommitmentRequest"
70
+ end
71
+ end
72
+
73
+ # Parses the XML into an array broken up by each event.
74
+ # Example of returned array
75
+ def parse_service(xml)
76
+ event_list = []
77
+ parse = Hpricot.parse(xml)/:error
78
+ if parse != []
79
+ RAILS_DEFAULT_LOGGER.info "#{xml}"
80
+ return (Hpricot.parse(xml)/:description).inner_html
81
+ else
82
+ return parse = (Hpricot.parse(xml)/:days).inner_html
83
+ end
84
+ end
85
+
86
+ def parse_express(xml)
87
+ parse = Hpricot.parse(xml)/:error
88
+ if parse != []
89
+ RAILS_DEFAULT_LOGGER.info "#{xml}"
90
+ return (Hpricot.parse(xml)/:description).inner_html
91
+ else
92
+ i= 0
93
+ location_list = []
94
+ (Hpricot.parse(xml)/:location).each do |location|
95
+ i+=1
96
+ h = {}
97
+ location.children.each {|elem| h[elem.name.to_sym] = elem.inner_text unless elem.inner_text.blank?}
98
+ location_list << h
99
+ end
100
+ return location_list
101
+ end
102
+ end
103
+
104
+ private
105
+ def commit_service_standard_request(action, request, test = false)
106
+ retries = MAX_RETRIES
107
+ begin
108
+ url = URI.parse(test ? "http://#{TEST_DOMAIN}#{TEST_RESOURCE}" : "http://#{LIVE_DOMAIN}#{LIVE_RESOURCE}")
109
+ req = Net::HTTP::Post.new(url.path)
110
+ req.set_form_data({'API' => API_CODES[action], 'XML' => request})
111
+ response = Net::HTTP.new(url.host, url.port)
112
+ response.open_timeout = 5
113
+ response.read_timeout = 5
114
+ response.start
115
+ rescue Timeout::Error
116
+ if retries > 0
117
+ retries -= 1
118
+ retry
119
+ else
120
+ RAILS_DEFAULT_LOGGER.warn "The connection to the remote server timed out"
121
+ return "We appoligize for the inconvience but our USPS service is busy at the moment. To retry please refresh the browser"
122
+
123
+ end
124
+ rescue SocketError
125
+ RAILS_DEFAULT_LOGGER.error "There is a socket error with USPS plugin"
126
+ return "We appoligize for the inconvience but there is a problem with our server. To retry please refresh the browser"
127
+ end
128
+
129
+ response = response.request(req)
130
+ case response
131
+ when Net::HTTPSuccess
132
+ if action == :express
133
+ parse_express(response.body)
134
+ else
135
+ parse_service(response.body)
136
+ end
137
+ else
138
+ RAILS_DEFAULT_LOGGER.warn("USPS plugin settings are wrong #{response}")
139
+ end
140
+ end
141
+
142
+ end
143
+ end