active_shipping 1.3.0 → 1.4.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.
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