active_shipping 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1797425b42f1d7cf823d470c1792b73137e942d8
4
- data.tar.gz: db1e589e17e44e30d20b7187daf10ee9b0836b8f
3
+ metadata.gz: 02e598e6f3f22df641b4384ef5bfac4ccf12adf5
4
+ data.tar.gz: 3f85700bd9c69ba3d5c3b21b84d7c42c9ec66487
5
5
  SHA512:
6
- metadata.gz: e1b1d52df207fbd4712ec3693d488126ddf5a01084bb0a1f55f566e8a8b4382bcfd4c7e17ddb0356fe25c1011ca80372cff6faeda1b95f09fa74d88028d410b8
7
- data.tar.gz: 8bbdc54774d6b015bfd6c918b510aeba12a2cc204cdf85be025736bd8ca38bfcdb7c9ea5be3855236fb1f62941bf72caea7d49dff35d9f56d48186dae9380b0a
6
+ metadata.gz: 34bdf173c3c6b0aee4b3ef7fdb36a4c502e87d4a79d14d9d5bff1a545638f97143ba9504424d77d6bfbd73f9f5c5659ac650e9adc1bd180d0974b5f9de39a0a4
7
+ data.tar.gz: 73b86a63d94164dc3a7c2f0fb3b3d7c5c1a6f2c248308426b3fffb96142d70a76cc572202d9b03aea745472871d66a7f6061d2eb233c7e0f69a6da6c8c3dba91
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # ActiveShipping CHANGELOG
2
2
 
3
+ ### v1.4.0
4
+
5
+ - Added support for USPS merchant returns service
6
+ - Added support for UPS SurePost
7
+ - Added support for UPS third party billing
8
+ - Fix FedEx tracking response errors
9
+ - Add rake console command for development
10
+
3
11
  ### v1.3.0
4
12
 
5
13
  - Support voiding labels on UPS
data/README.md CHANGED
@@ -16,6 +16,7 @@ Active Shipping is currently being used and improved in a production environment
16
16
 
17
17
  * [UPS](http://www.ups.com)
18
18
  * [USPS](http://www.usps.com)
19
+ * [USPS Returns](http://returns.usps.com)
19
20
  * [FedEx](http://www.fedex.com)
20
21
  * [Canada Post](http://www.canadapost.ca)
21
22
  * [New Zealand Post](http://www.nzpost.co.nz)
@@ -116,6 +117,10 @@ To log requests and responses, just set the `logger` on your carrier class to so
116
117
 
117
118
  (This logging functionality is provided by the [`PostsData` module](https://github.com/Shopify/active_utils/blob/master/lib/active_utils/posts_data.rb) in the `active_utils` dependency.)
118
119
 
120
+ To debug API requests and your code you can run `rake console` to start a Pry session with `ActiveShipping` included
121
+ and instances of the various carriers set up with your test credentials.
122
+ Look at the file `test/console.rb` to see the other goodies it provides.
123
+
119
124
  After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there! For more information, see CONTRIBUTING.md.
120
125
 
121
126
  ## Legal Mumbo Jumbo
data/Rakefile CHANGED
@@ -24,4 +24,9 @@ namespace :test do
24
24
  end
25
25
  end
26
26
 
27
+ desc "Open a pry session preloaded with this library"
28
+ task :console do
29
+ sh 'ruby -Ilib -Itest test/console.rb'
30
+ end
31
+
27
32
  task :default => 'test'
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.add_development_dependency('mocha', '~> 1')
25
25
  s.add_development_dependency('timecop')
26
26
  s.add_development_dependency('business_time')
27
+ s.add_development_dependency('pry')
27
28
 
28
29
  s.files = `git ls-files`.split($/)
29
30
  s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -43,3 +43,5 @@ require 'active_shipping/shipment_packer'
43
43
  require 'active_shipping/carrier'
44
44
  require 'active_shipping/carriers'
45
45
  require 'active_shipping/errors'
46
+ require 'active_shipping/external_return_label_request'
47
+ require 'active_shipping/external_return_label_response'
@@ -24,6 +24,7 @@ ActiveShipping::Carriers.register :BenchmarkCarrier, 'active_shipping/carriers/b
24
24
  ActiveShipping::Carriers.register :BogusCarrier, 'active_shipping/carriers/bogus_carrier'
25
25
  ActiveShipping::Carriers.register :UPS, 'active_shipping/carriers/ups'
26
26
  ActiveShipping::Carriers.register :USPS, 'active_shipping/carriers/usps'
27
+ ActiveShipping::Carriers.register :USPSReturns, 'active_shipping/carriers/usps_returns'
27
28
  ActiveShipping::Carriers.register :FedEx, 'active_shipping/carriers/fedex'
28
29
  ActiveShipping::Carriers.register :Shipwire, 'active_shipping/carriers/shipwire'
29
30
  ActiveShipping::Carriers.register :Kunaki, 'active_shipping/carriers/kunaki'
@@ -575,7 +575,7 @@ module ActiveShipping
575
575
  when '9040'
576
576
  raise ActiveShipping::ShipmentNotFound, first_notification.at('Message').text
577
577
  else
578
- raise ActiveShipping::ResponseContentError, first_notification.at('Message').text
578
+ raise ActiveShipping::ResponseContentError, StandardError.new(first_notification.at('Message').text)
579
579
  end
580
580
  end
581
581
 
@@ -65,8 +65,11 @@ module ActiveShipping
65
65
  "83" => "UPS Today Dedicated Courier",
66
66
  "84" => "UPS Today Intercity",
67
67
  "85" => "UPS Today Express",
68
- "86" => "UPS Today Express Saver"
69
-
68
+ "86" => "UPS Today Express Saver",
69
+ "92" => "UPS SurePost (USPS) < 1lb",
70
+ "93" => "UPS SurePost (USPS) > 1lb",
71
+ "94" => "UPS SurePost (USPS) BPM",
72
+ "95" => "UPS SurePost (USPS) Media",
70
73
  }
71
74
 
72
75
  CANADA_ORIGIN_SERVICES = {
@@ -375,6 +378,20 @@ module ActiveShipping
375
378
  end
376
379
  end
377
380
  end
381
+ elsif options[:bill_third_party]
382
+ xml.PaymentInformation do
383
+ xml.BillThirdParty do
384
+ xml.BillThirdPartyShipper do
385
+ xml.AccountNumber(options[:billing_account])
386
+ xml.ThirdParty do
387
+ xml.Address do
388
+ xml.PostalCode(options[:billing_zip])
389
+ xml.CountryCode(options[:billing_country])
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
378
395
  else
379
396
  xml.ItemizedPaymentInformation do
380
397
  xml.ShipmentCharge do
@@ -0,0 +1,86 @@
1
+ module ActiveShipping
2
+
3
+ class USPSReturns < Carrier
4
+
5
+ self.retry_safe = true
6
+
7
+ cattr_reader :name
8
+ @@name = "USPS Returns"
9
+
10
+ LIVE_DOMAIN = 'returns.usps.com'
11
+ LIVE_RESOURCE = 'Services/ExternalCreateReturnLabel.svc/ExternalCreateReturnLabel'
12
+
13
+ TEST_DOMAIN = 'returns.usps.com'
14
+ TEST_RESOURCE = 'Services/ExternalCreateReturnLabel.svc/ExternalCreateReturnLabel'
15
+
16
+ API_CODES = {
17
+ :external_return_label_request => 'externalReturnLabelRequest'
18
+ }
19
+
20
+ USE_SSL = {
21
+ :external_return_label_request => true
22
+ }
23
+
24
+ def requirements
25
+ []
26
+ end
27
+
28
+ def external_return_label_request(label, options = {})
29
+ response = commit(:external_return_label_request, label.to_xml, (options[:test] || false))
30
+ parse_external_return_label_response(response)
31
+ end
32
+
33
+ protected
34
+
35
+ def parse_external_return_label_response(response)
36
+ tracking_number, postal_routing, return_label, message = '', '', '', '', ''
37
+ xml = Nokogiri::XML(response)
38
+ error = external_return_label_errors(xml)
39
+ if error.is_a?(Hash) && error.size > 0
40
+ message << "#{error[:error][:code]}: #{error[:error][:message]}"
41
+ else
42
+ tracking_number = xml.at('TrackingNumber').try(:text)
43
+ postal_routing = xml.at('PostalRouting').try(:text)
44
+ return_label = xml.at('ReturnLabel').try(:text)
45
+ end
46
+
47
+ ExternalReturnLabelResponse.new(message.length == 0, message, Hash.from_xml(response),
48
+ :xml => response,
49
+ :carrier => @@name,
50
+ :request => last_request,
51
+ :return_label => return_label,
52
+ :postal_routing => postal_routing,
53
+ :tracking_number => tracking_number
54
+ )
55
+ end
56
+
57
+ def external_return_label_errors(document)
58
+ return {} unless document.respond_to?(:elements)
59
+ res = {}
60
+ if node = document.at('*/errors')
61
+ if node.at('ExternalReturnLabelError')
62
+ if message = node.at('ExternalReturnLabelError/InternalErrorDescription').try(:text)
63
+ code = node.at('ExternalReturnLabelError/InternalErrorNumber').try(:text) || ''
64
+ res = {:error => {:code => code, :message => message}}
65
+ elsif message = node.at('ExternalReturnLabelError/ExternalErrorDescription').try(:text)
66
+ code = node.at('ExternalReturnLabelError/ExternalErrorNumber').try(:text) || ''
67
+ res = {:error => {:code => code, :message => message}}
68
+ end
69
+ end
70
+ end
71
+ res
72
+ end
73
+
74
+ def commit(action, request, test = false)
75
+ ssl_get(request_url(action, request, test))
76
+ end
77
+
78
+ def request_url(action, request, test)
79
+ scheme = USE_SSL[action] ? 'https://' : 'http://'
80
+ host = test ? TEST_DOMAIN : LIVE_DOMAIN
81
+ resource = test ? TEST_RESOURCE : LIVE_RESOURCE
82
+ "#{scheme}#{host}/#{resource}?#{API_CODES[action]}=#{URI.encode(request)}"
83
+ end
84
+
85
+ end
86
+ end
@@ -16,11 +16,20 @@ module ActiveShipping
16
16
  end
17
17
 
18
18
  class ResponseContentError < ActiveShipping::Error
19
- def initialize(exception, content_body)
20
- super("#{exception.message} \n\n#{content_body}")
19
+ def initialize(exception, content_body = nil)
20
+ super([exception.message, content_body].compact.join(" \n\n"))
21
21
  end
22
22
  end
23
23
 
24
24
  class ShipmentNotFound < ActiveShipping::Error
25
25
  end
26
+
27
+ class USPSValidationError < StandardError
28
+ end
29
+
30
+ class USPSMissingRequiredTagError < StandardError
31
+ def initialize(tag, prop)
32
+ super("Missing required tag #{tag} set by property #{prop}")
33
+ end
34
+ end
26
35
  end
@@ -0,0 +1,421 @@
1
+ # -*- coding: utf-8 -*-
2
+ module ActiveShipping
3
+
4
+ class ExternalReturnLabelRequest
5
+
6
+ CAP_STRING_LEN = 100
7
+
8
+ USPS_EMAIL_REGEX = /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
9
+
10
+ LABEL_FORMAT = {
11
+ 'Instructions' => 'null',
12
+ 'No Instructions' => 'NOI',
13
+ 'Double Label' => 'TWO'
14
+ }
15
+
16
+ SERVICE_TYPE_CODE = [
17
+ '044', '019', '596', '020', '597','022', '024', '017', '018'
18
+ ]
19
+
20
+ CALL_CENTER_OR_SELF_SERVICE = ['CallCenter', 'Customer']
21
+
22
+ LABEL_DEFINITION = ['4X6', 'Zebra-4X6', '4X4', '3X6']
23
+
24
+ IMAGE_TYPE = ['PDF', 'TIF']
25
+
26
+ attr_reader :customer_name,
27
+ :customer_address1,
28
+ :customer_address2,
29
+ :customer_city,
30
+ :customer_state,
31
+ :customer_zipcode,
32
+ :customer_urbanization,
33
+ :company_name,
34
+ :attention,
35
+ :label_format,
36
+ :label_definition,
37
+ :service_type_code,
38
+ :merchandise_description,
39
+ :insurance_amount,
40
+ :address_override_notification,
41
+ :packaging_information,
42
+ :packaging_information2,
43
+ :call_center_or_self_service,
44
+ :image_type,
45
+ :address_validation,
46
+ :sender_name,
47
+ :sender_email,
48
+ :recipient_name,
49
+ :recipient_email,
50
+ :recipient_bcc,
51
+ :merchant_account_id,
52
+ :mid
53
+
54
+ def initialize(options = {})
55
+ options.each do |pair|
56
+ self.public_send("#{pair[0]}=".to_sym, pair[1]) if self.respond_to?("#{pair[0]}=".to_sym)
57
+ end
58
+
59
+ verify_or_raise_required
60
+ end
61
+
62
+ def self.from_hash(options={})
63
+ self.new(options)
64
+ end
65
+
66
+ # Sent by the system containing the returns label attachment and message.
67
+ def recipient_bcc=(v)
68
+ @recipient_bcc = validate_email(v, __method__)
69
+ end
70
+
71
+ # Sent by the system containing the returns label attachment and message.
72
+ # <em>Optional</em>.
73
+ def recipient_email=(v)
74
+ @recipient_email = validate_email(v, __method__)
75
+ end
76
+
77
+ # The name in an email sent by the system containing the returns label attachment.
78
+ # <em>Optional</em>.
79
+ def recipient_name=(v)
80
+ @recipient_name = nil
81
+ if (v = sanitize(v)) && v.length > 0
82
+ @recipient_name = v
83
+ else
84
+ raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
85
+ end
86
+ end
87
+
88
+ # The From address in an email sent by the system containing the returns
89
+ # label attachment and message, Defaults to DONOTREPLY@USPSReturns.com
90
+ # if a recipient email is entered and a sender email is not.
91
+ # <em>Optional</em>.
92
+ def sender_email=(v)
93
+ @sender_email = validate_email(v, __method__)
94
+ end
95
+
96
+ # The From name in an email sent by the system containing the returns
97
+ # label attachment. Defaults to “Merchant Returns” if a recipient name
98
+ # is entered and a sender name is not.
99
+ # <em>Optional</em>.
100
+ def sender_name=(v)
101
+ @sender_name = nil
102
+ if (v = sanitize(v)) && v.length > 0
103
+ @sender_name = v
104
+ else
105
+ raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
106
+ end
107
+ end
108
+
109
+ # Used to override the validation of the customer address.
110
+ # If true, the address will be validated against WebTools.
111
+ # If false, the system will bypass the validation.
112
+ # <em>Optional</em>.
113
+ def address_validation=(v)
114
+ @address_validation = to_bool(v, true)
115
+ end
116
+
117
+ # Used to select the format of the return label.
118
+ # <em>Optional</em>.
119
+ # * PDF <em>Default</em>.
120
+ # * TIF
121
+ def image_type=(v)
122
+ @image_type = validate_set_inclusion(v.to_s.upcase, IMAGE_TYPE, __method__)
123
+ end
124
+
125
+ # Used to determine if the returns label request is coming from a
126
+ # merchant call center agent or an end customer.
127
+ # <b>Required</b>.
128
+ # [CallCenter]
129
+ # [Customer]
130
+ def call_center_or_self_service=(v)
131
+ @call_center_or_self_service = validate_set_inclusion(v, CALL_CENTER_OR_SELF_SERVICE, __method__)
132
+ end
133
+
134
+ # Package information can be one of three types: RMA, Invoice or
135
+ # Order number. This will appear on the second label generated when
136
+ # the LabelFormat “TWO” is selected.
137
+ # <em>Optional</em>.
138
+ def packaging_information2=(v)
139
+ @packaging_information2 = validate_string_length(v, 15, __method__)
140
+ end
141
+
142
+ # Package information can be one of three types: RMA, Invoice or
143
+ # Order number. This will appear on the generated label.
144
+ # <em>Optional</em>.
145
+ def packaging_information=(v)
146
+ @packaging_information = validate_string_length(v, 15, __method__)
147
+ end
148
+
149
+ # Override address if more address information
150
+ # is needed or system cannot find address. If
151
+ # the address_override_notification value is
152
+ # true then any address error being passed from
153
+ # WebTools would be bypassed and a successful
154
+ # response will be sent.
155
+ # <b>Required</b>.
156
+ def address_override_notification=(v)
157
+ @address_validation = to_bool(v)
158
+ end
159
+
160
+ # Insured amount of package.
161
+ def insurance_amount=(v)
162
+ @insurance_amount = nil
163
+ if (1..200).include?(v.to_f)
164
+ @insurance_amount = v
165
+ else
166
+ raise USPSValidationError, "#{__method__} must be a numerical value between 1 and 200, found value '#{v}'."
167
+ end
168
+ end
169
+
170
+ # Description of the merchandise.
171
+ # <em>Optional</em>.
172
+ def merchandise_description=(v)
173
+ @merchandise_description = validate_string_length(v, 255, __method__)
174
+ end
175
+
176
+ # Service type of the label as specified in the merchant profile setup.
177
+ # <b>Required</b>.
178
+ # [044] (Parcel Return Service)
179
+ # [019] (Priority Mail Returns service)
180
+ # [596] (Priority Mail Returns service, Insurance <= $200)
181
+ # [020] (First-Class Package Return service)
182
+ # [597] (First-Class Package Return service, Insurance <= $200)
183
+ # [022] (Ground Return Service)
184
+ # [024] (PRS – Full Network)
185
+ # [017] (PRS – Full Network, Insurance <=$200)
186
+ # [018] (PRS – Full Network, Insurance >$200)
187
+ def service_type_code=(v)
188
+ @service_type_code = validate_set_inclusion(v, SERVICE_TYPE_CODE, __method__)
189
+ end
190
+
191
+ # Size of the label.
192
+ # <b>Required</b>.
193
+ # * 4X6
194
+ # * Zebra-4X6
195
+ # * 4X4
196
+ # * 3X6
197
+ def label_definition=(v)
198
+ @label_definition = validate_set_inclusion(v, LABEL_DEFINITION, __method__)
199
+ end
200
+
201
+ def label_format
202
+ @label_format && LABEL_FORMAT[@label_format]
203
+ end
204
+
205
+ # Format in which the label(s) will be printed.
206
+ # * null (“Instructions”)
207
+ # * NOI (“No Instructions”)
208
+ # * TWO (“Double Label”)
209
+ def label_format=(v)
210
+ @label_format = validate_set_inclusion(v, LABEL_FORMAT.keys, __method__)
211
+ end
212
+
213
+ # The intended recipient of the returned package (e.g. Returns Department).
214
+ # <em>Optional</em>.
215
+ def attention=(v)
216
+ @attention = validate_string_length(v, 38, __method__)
217
+ end
218
+
219
+ # The name of the company to which the package is being returned.
220
+ # <em>Optional</em>.
221
+ def company_name=(v)
222
+ @company_name = validate_string_length(v, 38, __method__)
223
+ end
224
+
225
+ # <b>Required</b>.
226
+ def merchant_account_id=(v)
227
+ @merchant_account_id = nil
228
+ if v.to_i > 0
229
+ @merchant_account_id = v
230
+ else
231
+ raise USPSValidationError, "#{__method__} must be a valid positive integer, found value '#{v}'."
232
+ end
233
+ end
234
+
235
+ # <b>Required</b>.
236
+ def mid=(v)
237
+ @mid = nil
238
+ if v.to_s =~ /^\d{6,9}$/
239
+ @mid = v
240
+ else
241
+ raise USPSValidationError, "#{__method__} must be a valid integer between 6 and 9 digits in length, found value '#{v}'."
242
+ end
243
+ end
244
+
245
+ # Urbanization of customer returning the package (only applicable to Puerto Rico addresses).
246
+ # <em>Optional</em>.
247
+ def customer_urbanization=(v)
248
+ @customer_urbanization = validate_string_length(v, 32, __method__)
249
+ end
250
+
251
+ # Name of customer returning package.
252
+ # <b>Required</b>.
253
+ def customer_name=(v)
254
+ @customer_name = validate_range(v, 1, 32, __method__)
255
+ end
256
+
257
+ # Address of the customer returning the package.
258
+ # <b>Required</b>.
259
+ def customer_address1=(v)
260
+ @customer_address1 = validate_range(v, 1, 32, __method__)
261
+ end
262
+
263
+ # Secondary address unit designator / number of customer
264
+ # returning the package. (such as an apartment or
265
+ # suite number, e.g. APT 202, STE 100)
266
+ def customer_address2=(v)
267
+ @customer_address2 = validate_range(v, 0, 32, __method__)
268
+ end
269
+
270
+ # City of customer returning the package.
271
+ # <b>Required</b>.
272
+ def customer_city=(v)
273
+ @customer_city = validate_range(v, 1, 32, __method__)
274
+ end
275
+
276
+ # State of customer returning the package.
277
+ # <b>Required</b>.
278
+ def customer_state=(v)
279
+ @customer_state = nil
280
+ if (v = sanitize(v)) && v =~ /^[a-zA-Z]{2}$/
281
+ @customer_state = v
282
+ else
283
+ raise USPSValidationError, "#{__method__} must be a String 2 chars in length, found value '#{v}'."
284
+ end
285
+ end
286
+
287
+ # Zipcode of customer returning the package.
288
+ # According to the USPS documentation, Zipcode is optional
289
+ # unless <tt>address_override_notification</tt> is true
290
+ # and <tt>address_validation</tt> is set to false.
291
+ # It's probably just easier to require Zipcodes.
292
+ # <b>Required</b>.
293
+ def customer_zipcode=(v)
294
+ @customer_zipcode = nil
295
+ if (v = sanitize(v))
296
+ v = v[0..4]
297
+ if v =~ /^\d{5}$/
298
+ @customer_zipcode = v
299
+ end
300
+ else
301
+ raise USPSValidationError, "#{__method__} must be a 5 digit number, found value '#{v}'."
302
+ end
303
+ end
304
+
305
+ def verify_or_raise_required
306
+ %w(customer_name customer_address1 customer_city customer_state
307
+ customer_zipcode label_format label_definition service_type_code
308
+ call_center_or_self_service).each do |attr|
309
+ raise USPSMissingRequiredTagError.new(attr.camelize, attr) unless send(attr.to_sym)
310
+ end
311
+ # Safer than using inflection acroynms
312
+ raise USPSMissingRequiredTagError.new("MID", "mid") unless mid
313
+ raise USPSMissingRequiredTagError.new("MerchantAccountID", "merchant_account_id") unless merchant_account_id
314
+ end
315
+
316
+ def to_xml
317
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
318
+ xml.ExternalReturnLabelRequest do
319
+ xml.CustomerName { xml.text(customer_name) }
320
+ xml.CustomerAddress1 { xml.text(customer_address1) }
321
+ xml.CustomerAddress2 { xml.text(customer_address2) } if customer_address2
322
+ xml.CustomerCity { xml.text(customer_city) }
323
+ xml.CustomerState { xml.text(customer_state) }
324
+ xml.CustomerZipCode { xml.text(customer_zipcode) } if customer_zipcode
325
+ xml.CustomerUrbanization { xml.text(customer_urbanization) } if customer_urbanization
326
+
327
+ xml.MerchantAccountID { xml.text(merchant_account_id) }
328
+ xml.MID { xml.text(mid) }
329
+
330
+ xml.SenderName { xml.text(sender_name) } if sender_name
331
+ xml.SenderEmail { xml.text(sender_email) } if sender_email
332
+
333
+ xml.RecipientName { xml.text(recipient_name) } if recipient_name
334
+ xml.RecipientEmail { xml.text(recipient_email) } if recipient_email
335
+ xml.RecipientBcc { xml.text(recipient_bcc) } if recipient_bcc
336
+
337
+ xml.LabelFormat { xml.text(label_format) } if label_format
338
+ xml.LabelDefinition { xml.text(label_definition) } if label_definition
339
+ xml.ServiceTypeCode { xml.text(service_type_code) } if service_type_code
340
+
341
+ xml.CompanyName { xml.text(company_name) } if company_name
342
+ xml.Attention { xml.text(attention) } if attention
343
+
344
+ xml.CallCenterOrSelfService { xml.text(call_center_or_self_service) }
345
+
346
+ xml.MerchandiseDescription { xml.text(merchandise_description) } if merchandise_description
347
+ xml.InsuranceAmount { xml.text(insurance_amount) } if insurance_amount
348
+
349
+ xml.AddressOverrideNotification { xml.text(!!address_override_notification) }
350
+
351
+ xml.PackageInformation { xml.text(packaging_information) } if packaging_information
352
+ xml.PackageInformation2 { xml.text(packaging_information2) } if packaging_information2
353
+
354
+ xml.ImageType { xml.text(image_type) } if image_type
355
+ xml.AddressValidation { xml.text(!!address_validation) }
356
+
357
+ end
358
+ end
359
+ xml_builder.to_xml
360
+ end
361
+
362
+ private
363
+
364
+ def to_bool(v, default = false)
365
+ v = v.to_s
366
+ if v =~ (/(true|yes|1)$/i)
367
+ true
368
+ elsif v =~ (/(false|no|0)$/i)
369
+ false
370
+ else
371
+ default
372
+ end
373
+ end
374
+
375
+ def sanitize(v)
376
+ if v.is_a?(String)
377
+ v.strip!
378
+ v[0..CAP_STRING_LEN - 1]
379
+ else
380
+ nil
381
+ end
382
+ end
383
+
384
+ def validate_range(v, min, max, meth)
385
+ if (v = sanitize(v).to_s) && ((min.to_i)..(max.to_i)).include?(v.length)
386
+ if v.length == 0
387
+ nil
388
+ else
389
+ v
390
+ end
391
+ else
392
+ raise USPSValidationError, "#{meth} must be a String between #{min.to_i} and #{max.to_i} chars in length, found value '#{v}'."
393
+ end
394
+ end
395
+
396
+ def validate_string_length(s, max_len, meth)
397
+ if (s = sanitize(s)) && s.length <= max_len.to_i
398
+ s
399
+ else
400
+ raise USPSValidationError, "#{meth} must be a String no more than #{max_len} chars in length, found value '#{s}'."
401
+ end
402
+ end
403
+
404
+ def validate_set_inclusion(v, set, meth)
405
+ if set.respond_to?(:include?) && set.include?(v)
406
+ v
407
+ else
408
+ raise USPSValidationError, "#{v} is not valid in #{meth}, try any of the following: #{(set.respond_to?(:join) && set.join(',')) || ''}"
409
+ end
410
+ end
411
+
412
+ def validate_email(v, meth)
413
+ if (v = sanitize(v)) && v =~ USPS_EMAIL_REGEX
414
+ v
415
+ else
416
+ raise USPSValidationError, "'#{v}' is not a valid e-mail in #{meth}"
417
+ end
418
+ end
419
+
420
+ end
421
+ end