stamps 0.2.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 (44) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +49 -0
  5. data/README.md +186 -0
  6. data/Rakefile +13 -0
  7. data/lib/stamps.rb +33 -0
  8. data/lib/stamps/api.rb +18 -0
  9. data/lib/stamps/client.rb +12 -0
  10. data/lib/stamps/client/account.rb +36 -0
  11. data/lib/stamps/client/address.rb +16 -0
  12. data/lib/stamps/client/rate.rb +39 -0
  13. data/lib/stamps/client/stamp.rb +62 -0
  14. data/lib/stamps/configuration.rb +69 -0
  15. data/lib/stamps/errors.rb +34 -0
  16. data/lib/stamps/mapping.rb +247 -0
  17. data/lib/stamps/request.rb +55 -0
  18. data/lib/stamps/response.rb +71 -0
  19. data/lib/stamps/trash.rb +29 -0
  20. data/lib/stamps/types.rb +68 -0
  21. data/lib/stamps/version.rb +3 -0
  22. data/stamps.gemspec +33 -0
  23. data/test/client/account_test.rb +64 -0
  24. data/test/client/address_test.rb +36 -0
  25. data/test/client/rate_test.rb +44 -0
  26. data/test/client/stamp_test.rb +89 -0
  27. data/test/fixtures/AuthenticateUser.xml +9 -0
  28. data/test/fixtures/CancelIndicium.xml +8 -0
  29. data/test/fixtures/CarrierPickup.xml +11 -0
  30. data/test/fixtures/CleanseAddress.xml +24 -0
  31. data/test/fixtures/CreateIndicium.xml +32 -0
  32. data/test/fixtures/GetAccountInfo.xml +87 -0
  33. data/test/fixtures/GetRate.xml +163 -0
  34. data/test/fixtures/GetRates.xml +645 -0
  35. data/test/fixtures/InsufficientPostage.xml +14 -0
  36. data/test/fixtures/InvalidSoap.xml +13 -0
  37. data/test/fixtures/PurchasePostage.xml +16 -0
  38. data/test/fixtures/TrackShipment.xml +43 -0
  39. data/test/helper.rb +58 -0
  40. data/test/mapping_test.rb +33 -0
  41. data/test/response_test.rb +66 -0
  42. data/test/stamps_test.rb +26 -0
  43. data/test/types_test.rb +25 -0
  44. metadata +228 -0
@@ -0,0 +1,69 @@
1
+ module Stamps
2
+ # Defines constants and methods related to configuration
3
+ module Configuration
4
+
5
+ # An array of valid keys in the options hash when configuring a {Twitter::API}
6
+ VALID_OPTIONS_KEYS = [
7
+ :integration_id,
8
+ :username,
9
+ :password,
10
+ :namespace,
11
+ :format,
12
+ :return_address,
13
+ :test_mode,
14
+ :raise_errors,
15
+ :log_messages,
16
+ :endpoint].freeze
17
+
18
+ # The endpoint that will be used to connect if none is set
19
+ DEFAULT_ENDPOINT = 'https://swsim.testing.stamps.com/swsim/swsimv12.asmx'.freeze
20
+
21
+ # The default namespace used on Stamps.com wsdl
22
+ DEFAULT_NAMESPACE = 'http://stamps.com/xml/namespace/2010/11/swsim/swsimv12'
23
+
24
+ # @note JSON is preferred over XML because it is more concise and faster to parse.
25
+ DEFAULT_FORMAT = :hash
26
+
27
+ # The user agent that will be sent to the API endpoint if none is set
28
+ DEFAULT_USER_AGENT = "Stamps Ruby Gem".freeze
29
+
30
+ # Do not raise errors by default
31
+ DEFAULT_RAISE_ERRORS = false
32
+
33
+ # Do not log requests and response by default
34
+ DEFAULT_LOG_MESSAGES = false
35
+
36
+ # @private
37
+ attr_accessor *VALID_OPTIONS_KEYS
38
+
39
+ # When this module is extended, set all configuration options to their default values
40
+ def self.extended(base)
41
+ base.reset
42
+ end
43
+
44
+ # Convenience method to allow configuration options to be set in a block
45
+ def configure
46
+ yield self
47
+
48
+ HTTPI.log = false
49
+ Savon.configure do |config|
50
+ config.log = self.log_messages
51
+ config.raise_errors = self.raise_errors
52
+ end
53
+ end
54
+
55
+ # Create a hash of options and their values
56
+ def options
57
+ Hash[VALID_OPTIONS_KEYS.map {|key| [key, send(key)] }]
58
+ end
59
+
60
+ # Reset all configuration options to defaults
61
+ def reset
62
+ self.endpoint = DEFAULT_ENDPOINT
63
+ self.namespace = DEFAULT_NAMESPACE
64
+ self.format = DEFAULT_FORMAT
65
+ self.log_messages = DEFAULT_LOG_MESSAGES
66
+ self.raise_errors = DEFAULT_RAISE_ERRORS
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,34 @@
1
+ module Stamps
2
+
3
+ # Custom error class for rescuing from all Stamps.com errors
4
+ class Error < StandardError
5
+ attr_reader :data
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ super
10
+ end
11
+ end
12
+
13
+ # Raised when Stamps.com returns the HTTP status code 400
14
+ class BadRequest < Error; end
15
+
16
+ # Raised when Stamps.com returns the HTTP status code 401
17
+ class Unauthorized < Error; end
18
+
19
+ # Raised when Stamps.com returns the HTTP status code 403
20
+ class Forbidden < Error; end
21
+
22
+ # Raised when Stamps.com returns the HTTP status code 404
23
+ class NotFound < Error; end
24
+
25
+ # Raised when Stamps.com returns the HTTP status code 406
26
+ class NotAcceptable < Error; end
27
+
28
+ # Raised when Stamps.com returns the HTTP status code 500
29
+ class InternalServerError < Error; end
30
+
31
+ # Raised when Stamps.com returns the HTTP status code 503
32
+ class ServiceUnavailable < Error; end
33
+
34
+ end
@@ -0,0 +1,247 @@
1
+ module Stamps
2
+
3
+ # == Mapping Module
4
+ #
5
+ # Provides an interface to convert hash keys and values into a
6
+ # hash that can be easily coverted to an xml document that the
7
+ # web service can understand.
8
+ #
9
+ #
10
+ module Mapping
11
+
12
+ class Account < Hashie::Trash
13
+ property :Authenticator, :from => :authenticator
14
+ end
15
+
16
+ class AuthenticateUser < Hashie::Trash
17
+ property :Credentials, :from => :credentials
18
+
19
+ def credentials=(val)
20
+ self[:Credentials] = Credentials.new(val)
21
+ end
22
+ end
23
+
24
+ class Credentials < Hashie::Trash
25
+ property :IntegrationID, :from => :integration_id
26
+ property :Username, :from => :username
27
+ property :Password, :from => :password
28
+ end
29
+
30
+ class Rates < Hashie::Trash
31
+ property :Authenticator, :from => :authenticator
32
+ property :Rate, :from => :rate
33
+ end
34
+
35
+ class Rate < Hashie::Trash
36
+ property :FromZIPCode, :from => :from_zip_code
37
+ property :ToZIPCode, :from => :to_zip_code
38
+ property :ToCountry, :from => :to_country
39
+ property :Amount, :from => :amount
40
+ property :MaxAmount, :from => :max_amount
41
+ property :ServiceType, :from => :service_type
42
+ property :PrintLayout, :from => :print_layout
43
+ property :DeliverDays, :from => :deliver_days
44
+ property :Error, :from => :error
45
+ property :WeightLb, :from => :weight_lb
46
+ property :WeightOz, :from => :weight_oz
47
+ property :PackageType, :from => :package_type
48
+ property :RequiresAllOf, :from => :requires_all
49
+ property :Length, :from => :length
50
+ property :Width, :from => :width
51
+ property :Height, :from => :height
52
+ property :ShipDate, :from => :ship_date
53
+ property :InsuredValue, :from => :insured_value
54
+ property :RegisteredValue, :from => :registration_value
55
+ property :CODValue, :from => :cod_value
56
+ property :DeclaredValue, :from => :declared_value
57
+ property :NonMachinable, :from => :non_machinable
58
+ property :RectangularShaped, :from => :rectangular
59
+ property :Prohibitions, :from => :prohibitions
60
+ property :Restrictions, :from => :restrictions
61
+ property :Observations, :from => :observations
62
+ property :Regulations, :from => :regulations
63
+ property :GEMNotes, :from => :gem_notes
64
+ property :MaxDimensions, :from => :max_dimensions
65
+ property :DimWeighting, :from => :dim_weighting
66
+ property :AddOns, :from => :add_ons
67
+ property :EffectiveWeightInOunces, :from => :effective_weight_in_ounces
68
+ property :IsIntraBMC, :from => :is_intra_bmc
69
+ property :Zone, :from => :zone
70
+ property :RateCategory, :from => :rate_category
71
+ property :ToState, :from => :to_state
72
+ property :CubicPricing, :from => :cubic_pricing
73
+
74
+ # Maps :rate to AddOns map
75
+ def add_ons=(addons)
76
+ self[:AddOns] = AddOnsArray.new(:add_on => addons[:add_on])
77
+ end
78
+ end
79
+
80
+ class AddOnsArray < Hashie::Trash
81
+ property :AddOnV2, :from => :add_on
82
+ def add_on=(vals)
83
+ return unless vals
84
+ self[:AddOnV2] = vals.map{ |value| AddOnV2.new(value).to_hash }
85
+ end
86
+ end
87
+
88
+ class AddOnV2 < Hashie::Trash
89
+ property :Amount, :from => :amount
90
+ property :AddOnType, :from => :type
91
+ end
92
+
93
+ class Stamp < Hashie::Trash
94
+ property :Authenticator, :from => :authenticator
95
+ property :IntegratorTxID, :from => :transaction_id
96
+ property :TrackingNumber, :from => :tracking_number
97
+ property :Rate, :from => :rate
98
+ property :From, :from => :from
99
+ property :To, :from => :to
100
+ property :CustomerID, :from => :customer_id
101
+ property :Customs, :from => :customs
102
+ property :SampleOnly, :from => :sample
103
+ property :ImageType, :from => :image_type
104
+ property :EltronPrinterDPIType, :from => :label_resolution
105
+ property :memo
106
+ property :recipient_email, :from => :recipient_email
107
+ property :deliveryNotification, :from => :notify
108
+ property :shipmentNotificationCC, :from => :notify_crates
109
+ property :shipmentNotificationFromCompany, :from => :notify_from_company
110
+ property :shipmentNotificationCompanyInSubject, :from => :notify_in_subject
111
+ property :rotationDegrees, :from => :rotation
112
+ property :printMemo, :from => :print_memo
113
+
114
+ # Maps :from to Address map
115
+ def from=(val)
116
+ # Set the defult :from address from address
117
+ self[:From] = Address.new(Stamps.return_address.merge!(val))
118
+ end
119
+
120
+ # Maps :to to Address map
121
+ def to=(val)
122
+ self[:To] = Address.new(val)
123
+ end
124
+
125
+ # Maps :rate to Rate map
126
+ def rate=(val)
127
+ self[:Rate] = Rate.new(val)
128
+ end
129
+
130
+ # Maps :customs to Customs map
131
+ def customs=(val)
132
+ self[:Customs] = Customs.new(val)
133
+ end
134
+ end
135
+
136
+ class Address < Hashie::Trash
137
+ property :FullName, :from => :full_name
138
+ property :NamePrefix, :from => :name_prefix
139
+ property :FirstName, :from => :first_name
140
+ property :MiddleName, :from => :middle_name
141
+ property :LastName, :from => :last_name
142
+ property :NameSuffix, :from => :name_suffex
143
+ property :Title, :from => :title
144
+ property :Department, :from => :deparartment
145
+ property :Company, :from => :company
146
+ property :Address1, :from => :address1
147
+ property :Address2, :from => :address2
148
+ property :City, :from => :city
149
+ property :State, :from => :state
150
+ property :ZIPCode, :from => :zip_code
151
+ property :ZIPCodeAddOn, :from => :zip_code_add_on
152
+ property :DPB, :from => :dpb
153
+ property :CheckDigit, :from => :check_digit
154
+ property :Province, :from => :province
155
+ property :PostalCode, :from => :postal_code
156
+ property :Country, :from => :country
157
+ property :Urbanization, :from => :urbanization
158
+ property :PhoneNumber, :from => :phone_number
159
+ property :Extension, :from => :extentsion
160
+ property :CleanseHash, :from => :cleanse_hash
161
+ property :OverrideHash, :from => :override_hash
162
+ end
163
+
164
+ class CleanseAddress < Hashie::Trash
165
+ property :Authenticator, :from => :authenticator
166
+ property :Address, :from => :address
167
+
168
+ # Maps :address to Address map
169
+ def address=(val)
170
+ self[:Address] = Address.new(val)
171
+ end
172
+ end
173
+
174
+ class PurchasePostage < Hashie::Trash
175
+ property :Authenticator, :from => :authenticator
176
+ property :PurchaseAmount, :from => :amount
177
+ property :ControlTotal, :from => :control_total
178
+ end
179
+
180
+ class CancelStamp< Hashie::Trash
181
+ property :Authenticator, :from => :authenticator
182
+ property :StampsTxID, :from => :transaction_id
183
+ property :TrackingNumber, :from => :tracking_number
184
+ end
185
+
186
+ class CarrierPickup < Hashie::Trash
187
+ property :Authenticator, :from => :authenticator
188
+ property :FirstName, :from => :first_name
189
+ property :LastName, :from => :last_name
190
+ property :Company, :from => :company
191
+ property :Address, :from => :address
192
+ property :SuiteOrApt, :from => :suite, :default => ''
193
+ property :City, :from => :city
194
+ property :State, :from => :state
195
+ property :ZIP, :from => :zip
196
+ property :ZIP4, :from => :zip_four
197
+ property :PhoneNumber, :from => :phone
198
+ property :PhoneExt, :from => :phone_ext
199
+ property :NumberOfExpressMailPieces, :from => :express_mail_count
200
+ property :NumberOfPriorityMailPieces, :from => :priority_mail_count
201
+ property :NumberOfInternationalPieces, :from => :international_mail_count
202
+ property :NumberOfOtherPieces, :from => :other_mail_count
203
+ property :TotalWeightOfPackagesLbs, :from => :total_weight
204
+ property :PackageLocation, :from => :location
205
+ property :SpecialInstruction, :from => :special_instruction
206
+ end
207
+
208
+ class Customs < Hashie::Trash
209
+ property :ContentType, :from => :content_type
210
+ property :Comments, :from => :comments
211
+ property :LicenseNumber, :from => :license_number
212
+ property :CertificateNumber, :from => :certificate_number
213
+ property :InvoiceNumber, :from => :invoice_number
214
+ property :OtherDescribe, :from => :other_describe
215
+ property :CustomsLines, :from => :customs_lines
216
+
217
+ # Maps :customs CustomsLine map
218
+ def customs_lines=(customs)
219
+ # Important: Must call to_hash to force re-ordering!
220
+ self[:CustomsLines] = customs.collect{ |val| CustomsLinesArray.new(val).to_hash }
221
+ end
222
+ end
223
+
224
+ class CustomsLinesArray < Hashie::Trash
225
+ property :CustomsLine, :from => :custom
226
+ def custom=(val)
227
+ self[:CustomsLine] = CustomsLine.new(val).to_hash
228
+ end
229
+ end
230
+
231
+ class CustomsLine < Hashie::Trash
232
+ property :Description, :from => :description
233
+ property :Quantity, :from => :quantity
234
+ property :Value, :from => :value
235
+ property :WeightLb, :from => :weight_lb
236
+ property :WeightOz, :from => :weight_oz
237
+ property :HSTariffNumber, :from => :hs_tariff_number
238
+ property :CountryOfOrigin, :from => :country_of_origin
239
+ end
240
+
241
+ class TrackShipment < Hashie::Trash
242
+ property :Authenticator, :from => :authenticator
243
+ property :StampsTxID, :from => :stamps_transaction_id
244
+ end
245
+
246
+ end
247
+ end
@@ -0,0 +1,55 @@
1
+ require 'hashie'
2
+ require 'savon'
3
+ require File.expand_path('../response', __FILE__)
4
+
5
+ module Stamps
6
+
7
+ # Defines HTTP request methods
8
+ module Request
9
+
10
+ # Perform an HTTP request
11
+ def request(web_method, params, raw=false)
12
+ client = Savon::Client.new do |wsdl, http|
13
+ wsdl.endpoint = self.endpoint
14
+ wsdl.namespace = self.namespace
15
+ end
16
+
17
+ response = client.request :tns, web_method do
18
+ http.headers = { "SoapAction" => formatted_soap_action(web_method) }
19
+ soap.namespace = 'tns'
20
+ soap.element_form_default = :qualified
21
+ soap.env_namespace = 'soap'
22
+ soap.namespaces["xmlns:tns"] = self.namespace
23
+ soap.body = params.to_hash
24
+ end
25
+ Stamps::Response.new(response).to_hash
26
+ end
27
+
28
+ # Get the Authenticator token. By using an Authenticator, the integration
29
+ # can be sure that the conversation between the integration and the SWS
30
+ # server is kept in sync and no messages have been lost.
31
+ #
32
+ def authenticator_token
33
+ @authenticator ||= self.get_authenticator_token
34
+ end
35
+
36
+ # Make Authentication request for the user
37
+ #
38
+ def get_authenticator_token
39
+ self.request('AuthenticateUser',
40
+ Stamps::Mapping::AuthenticateUser.new(
41
+ :credentials => {
42
+ :integration_id => self.integration_id,
43
+ :username => self.username,
44
+ :password => self.password
45
+ })
46
+ )[:authenticate_user_response][:authenticator]
47
+ end
48
+
49
+ # Concatenates namespace and web method in a way the API can understand
50
+ def formatted_soap_action(web_method)
51
+ [self.namespace, web_method.to_s].compact.join('/')
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,71 @@
1
+ module Stamps
2
+
3
+ # = Stamps::Response
4
+ #
5
+ # Represents the response and contains the HTTP response.
6
+ #
7
+ class Response
8
+
9
+ # Expects an <tt>Savon::SOAP::Response</tt> and handles errors.
10
+ def initialize(response)
11
+ self.errors = []
12
+ self.valid = true
13
+ self.savon = response
14
+ self.http = response.http
15
+ self.hash = self.savon.to_hash
16
+ raise_errors
17
+ end
18
+
19
+ attr_accessor :savon, :http, :errors, :valid, :hash, :code
20
+
21
+ # Returns the SOAP response body as a Hash.
22
+ def to_hash
23
+ self.hash.merge!(:errors => self.errors)
24
+ self.hash.merge!(:valid? => self.valid)
25
+ self.hash
26
+ Stamps.format.to_s.downcase == 'hashie' ? Hashie::Mash.new(@hash) : self.hash
27
+ end
28
+
29
+ # Um, there's gotta be a better way
30
+ def valid?
31
+ self.valid
32
+ end
33
+
34
+ # Process any errors we get back from the service.
35
+ # Wrap any internal errors (from Soap Faults) into an array
36
+ # so that clients can process the error messages as they wish
37
+ #
38
+ def raise_errors
39
+ message = 'FIXME: Need to parse http for response message'
40
+ return self.format_soap_faults if savon.soap_fault.present?
41
+
42
+ case http.code.to_i
43
+ when 200
44
+ return
45
+ when 400
46
+ raise BadRequest, "(#{http.code}): #{message}"
47
+ when 401
48
+ raise Unauthorized, "(#{http.code}): #{message}"
49
+ when 403
50
+ raise Forbidden, "(#{http.code}): #{message}"
51
+ when 404
52
+ raise NotFound, "(#{http.code}): #{message}"
53
+ when 406
54
+ raise NotAcceptable, "(#{http.code}): #{message}"
55
+ when 500
56
+ raise InternalServerError, "Stamps.com had an internal error. (#{http.code}): #{message}"
57
+ when 502..503
58
+ raise ServiceUnavailable, "(#{http.code}): #{message}"
59
+ end
60
+ end
61
+
62
+ # Include any errors in the response
63
+ #
64
+ def format_soap_faults
65
+ fault = self.hash.delete(:fault)
66
+ self.errors << fault[:faultstring]
67
+ self.valid = false
68
+ end
69
+
70
+ end
71
+ end