active_shipping 0.12.6 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/{CHANGELOG → CHANGELOG.md} +6 -2
  5. data/CONTRIBUTING.md +32 -0
  6. data/{README.markdown → README.md} +45 -61
  7. data/lib/active_shipping.rb +20 -28
  8. data/lib/active_shipping/carrier.rb +82 -0
  9. data/lib/active_shipping/carriers.rb +33 -0
  10. data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
  11. data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
  12. data/lib/active_shipping/carriers/canada_post.rb +253 -0
  13. data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
  14. data/lib/active_shipping/carriers/fedex.rb +579 -0
  15. data/lib/active_shipping/carriers/kunaki.rb +164 -0
  16. data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
  17. data/lib/active_shipping/carriers/shipwire.rb +181 -0
  18. data/lib/active_shipping/carriers/stamps.rb +861 -0
  19. data/lib/active_shipping/carriers/ups.rb +648 -0
  20. data/lib/active_shipping/carriers/usps.rb +642 -0
  21. data/lib/active_shipping/errors.rb +7 -0
  22. data/lib/active_shipping/label_response.rb +23 -0
  23. data/lib/active_shipping/location.rb +149 -0
  24. data/lib/active_shipping/package.rb +241 -0
  25. data/lib/active_shipping/rate_estimate.rb +64 -0
  26. data/lib/active_shipping/rate_response.rb +13 -0
  27. data/lib/active_shipping/response.rb +41 -0
  28. data/lib/active_shipping/shipment_event.rb +17 -0
  29. data/lib/active_shipping/shipment_packer.rb +73 -0
  30. data/lib/active_shipping/shipping_response.rb +12 -0
  31. data/lib/active_shipping/tracking_response.rb +52 -0
  32. data/lib/active_shipping/version.rb +1 -1
  33. data/lib/vendor/quantified/test/length_test.rb +2 -2
  34. data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
  35. metadata +58 -36
  36. metadata.gz.sig +0 -0
  37. data/lib/active_shipping/shipping/base.rb +0 -13
  38. data/lib/active_shipping/shipping/carrier.rb +0 -84
  39. data/lib/active_shipping/shipping/carriers.rb +0 -23
  40. data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
  41. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
  42. data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
  43. data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
  44. data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
  45. data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
  46. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
  47. data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
  48. data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
  49. data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
  50. data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
  51. data/lib/active_shipping/shipping/errors.rb +0 -9
  52. data/lib/active_shipping/shipping/label_response.rb +0 -25
  53. data/lib/active_shipping/shipping/location.rb +0 -152
  54. data/lib/active_shipping/shipping/package.rb +0 -243
  55. data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
  56. data/lib/active_shipping/shipping/rate_response.rb +0 -15
  57. data/lib/active_shipping/shipping/response.rb +0 -43
  58. data/lib/active_shipping/shipping/shipment_event.rb +0 -19
  59. data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
  60. data/lib/active_shipping/shipping/shipping_response.rb +0 -14
  61. data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -0,0 +1,861 @@
1
+ require 'builder'
2
+
3
+ module ActiveShipping
4
+ # Stamps.com integration for rating, tracking, address validation, and label generation
5
+ # Integration ID can be requested from Stamps.com
6
+
7
+ class Stamps < Carrier
8
+ self.ssl_version = :SSLv3
9
+
10
+ cattr_reader :name
11
+ @@name = 'Stamps'
12
+
13
+ attr_reader :last_swsim_method
14
+
15
+ # TODO: Update to latest API. Documentation for the latest WSDL version is available here: http://support.stamps.com/outgoing/swsimv39doc.zip
16
+ LIVE_URL = 'https://swsim.stamps.com/swsim/swsimv34.asmx'
17
+ TEST_URL = 'https://swsim.testing.stamps.com/swsim/swsimv34.asmx'
18
+ NAMESPACE = 'http://stamps.com/xml/namespace/2014/01/swsim/swsimv34'
19
+
20
+ REQUIRED_OPTIONS = [:integration_id, :username, :password].freeze
21
+
22
+ PACKAGE = [
23
+ 'Postcard',
24
+ 'Letter',
25
+ 'Large Envelope or Flat',
26
+ 'Thick Envelope',
27
+ 'Package',
28
+ 'Flat Rate Box',
29
+ 'Small Flat Rate Box',
30
+ 'Large Flat Rate Box',
31
+ 'Flat Rate Envelope',
32
+ 'Flat Rate Padded Envelope',
33
+ 'Large Package',
34
+ 'Oversized Package',
35
+ 'Regional Rate Box A',
36
+ 'Regional Rate Box B',
37
+ 'Regional Rate Box C',
38
+ 'Legal Flat Rate Envelope'
39
+ ].freeze
40
+
41
+ US_POSSESSIONS = %w(AS FM GU MH MP PW PR VI)
42
+
43
+ SERVICE_TYPES = {
44
+ 'US-FC' => 'USPS First-Class Mail',
45
+ 'US-MM' => 'USPS Media Mail',
46
+ 'US-PM' => 'USPS Priority Mail',
47
+ 'US-BP' => 'USPS BP',
48
+ 'US-LM' => 'USPS LM',
49
+ 'US-XM' => 'USPS Express Mail',
50
+ 'US-EMI' => 'USPS Express Mail International',
51
+ 'US-PMI' => 'USPS Priority Mail International',
52
+ 'US-FCI' => 'USPS First Class Mail International',
53
+ 'US-CM' => 'USPS Critical Mail',
54
+ 'US-PS' => 'USPS Parcel Select'
55
+ }
56
+
57
+ ADD_ONS = {
58
+ 'SC-A-HP' => 'Hidden Postage',
59
+ 'SC-A-INS' => 'Insurance',
60
+ 'SC-A-INSRM' => 'Insurance for Registered Mail',
61
+ 'US-A-CM' => 'Certified Mail',
62
+ 'US-A-COD' => 'Collect on Delivery',
63
+ 'US-A-COM' => 'Certificate of Mailing',
64
+ 'US-A-DC' => 'USPS Delivery Confirmation',
65
+ 'US-A-ESH' => 'USPS Express - Sunday / Holiday Guaranteed',
66
+ 'US-A-INS' => 'USPS Insurance',
67
+ 'US-A-NDW' => 'USPS Express - No Delivery on Saturdays',
68
+ 'US-A-RD' => 'Restricted Delivery',
69
+ 'US-A-REG' => 'Registered Mail',
70
+ 'US-A-RR' => 'Return Receipt Requested',
71
+ 'US-A-RRM' => 'Return Receipt for Merchandise',
72
+ 'US-A-SC' => 'USPS Signature Confirmation',
73
+ 'US-A-SH' => 'Special Handling',
74
+ 'US-A-NND' => 'Notice of non-delivery',
75
+ 'US-A-SR' => 'Unknow Service Name SR',
76
+ 'US-A-RRE' => 'Unknow Service Name RRE'
77
+ }
78
+
79
+ CARRIER_PICKUP_LOCATION = {
80
+ 'FrontDoor' => 'Packages are at front door',
81
+ 'BackDoor' => 'Packages are at back door',
82
+ 'SideDoor' => 'Packages are at side door',
83
+ 'KnockOnDoorOrRingBell' => 'Knock on door or ring bell',
84
+ 'MailRoom' => 'Packages are in mail room',
85
+ 'Office' => 'Packages are in office',
86
+ 'Reception' => 'Packages are at reception area',
87
+ 'InOrAtMailbox' => 'Packages are in mail box',
88
+ 'Other' => 'Other Location'
89
+ }
90
+
91
+ PRINT_LAYOUTS = [
92
+ 'Normal',
93
+ 'NormalLeft',
94
+ 'NormalRight',
95
+ 'Normal4X6',
96
+ 'Normal6X4',
97
+ 'Normal75X2',
98
+ 'NormalReceipt',
99
+ 'NormalCN22',
100
+ 'NormalCP72',
101
+ 'Normal4X6CN22',
102
+ 'Normal6X4CN22',
103
+ 'Normal4X6CP72',
104
+ 'Normal6X4CP72',
105
+ 'Normal4X675',
106
+ 'Normal4X675CN22',
107
+ 'Normal4X675CP72',
108
+ 'Return',
109
+ 'ReturnCN22',
110
+ 'ReturnCP72',
111
+ 'Return4X675',
112
+ 'Return4X675CN22',
113
+ 'Return4X675CP72',
114
+ 'SDC3510',
115
+ 'SDC3520',
116
+ 'SDC3530',
117
+ 'SDC3610',
118
+ 'SDC3710',
119
+ 'SDC3810',
120
+ 'SDC3820',
121
+ 'SDC3910',
122
+ 'Envelope9',
123
+ 'Envelope10',
124
+ 'Envelope11',
125
+ 'Envelope12',
126
+ 'EnvelopePersonal',
127
+ 'EnvelopeMonarch',
128
+ 'EnvelopeInvitation',
129
+ 'EnvelopeGreeting'
130
+ ]
131
+
132
+ IMAGE_TYPE = %w(Auto Epl Gif Jpg Pdf Png Zpl)
133
+
134
+ def account_info
135
+ request = build_get_account_info_request
136
+ commit(:GetAccountInfo, request)
137
+ end
138
+
139
+ def purchase_postage(purchase_amount, control_total)
140
+ request = build_purchase_postage_request(purchase_amount, control_total)
141
+ commit(:PurchasePostage, request)
142
+ end
143
+
144
+ def purchase_status(transaction_id)
145
+ request = build_get_purchase_status(transaction_id)
146
+ commit(:GetPurchaseStatus, request)
147
+ end
148
+
149
+ def validate_address(address, options = {})
150
+ address = standardize_address(address)
151
+ request = build_cleanse_address_request(address)
152
+ commit(:CleanseAddress, request)
153
+ end
154
+
155
+ def find_rates(origin, destination, package, options = {})
156
+ origin = standardize_address(origin)
157
+ destination = standardize_address(destination)
158
+ request = build_rate_request(origin, destination, package, options)
159
+ commit(:GetRates, request)
160
+ end
161
+
162
+ def create_shipment(origin, destination, package, line_items = [], options = {})
163
+ origin = standardize_address(origin)
164
+ destination = standardize_address(destination)
165
+ request = build_create_indicium_request(origin, destination, package, line_items, options)
166
+ commit(:CreateIndicium, request)
167
+ end
168
+
169
+ def find_tracking_info(shipment_id, options = {})
170
+ request = build_track_shipment_request(shipment_id, options)
171
+ commit(:TrackShipment, request)
172
+ end
173
+
174
+ def namespace
175
+ NAMESPACE
176
+ end
177
+
178
+ def clear_authenticator
179
+ @authenticator = nil
180
+ end
181
+
182
+ private
183
+
184
+ def requirements
185
+ REQUIRED_OPTIONS
186
+ end
187
+
188
+ def save_swsim_method(swsim_method)
189
+ @last_swsim_method = swsim_method
190
+ end
191
+
192
+ def international?(address)
193
+ ! (['US', nil] + US_POSSESSIONS).include?(address.country_code)
194
+ end
195
+
196
+ def standardize_address(address)
197
+ if US_POSSESSIONS.include?(address.country_code)
198
+ new_address = address.to_hash
199
+ new_address[:province] = new_address[:country]
200
+ new_address[:country] = 'US'
201
+ Location.new(new_address)
202
+ else
203
+ address
204
+ end
205
+ end
206
+
207
+ def domestic?(address)
208
+ address.country_code(:alpha2) == 'US' || address.country_code(:alpha2).nil?
209
+ end
210
+
211
+ def authenticator
212
+ get_authenticator unless @authenticator
213
+ @authenticator
214
+ end
215
+
216
+ def renew_authenticator(request)
217
+ old_authenticator = authenticator
218
+ clear_authenticator
219
+ request.sub(old_authenticator, authenticator)
220
+ end
221
+
222
+ def get_authenticator
223
+ request = build_authenticate_user_request
224
+ commit(:AuthenticateUser, request)
225
+ end
226
+
227
+ def build_header
228
+ xml = Builder::XmlMarkup.new
229
+ xml.instruct!
230
+ xml.soap(:Envelope,
231
+ 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
232
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
233
+ 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
234
+ 'xmlns:tns' => 'http://stamps.com/xml/namespace/2014/01/swsim/swsimv34'
235
+ ) do
236
+ xml.soap :Body do
237
+ yield(xml)
238
+ end
239
+ end
240
+ end
241
+
242
+ def build_authenticate_user_request
243
+ build_header do |xml|
244
+ xml.tns :AuthenticateUser do
245
+ xml.tns :Credentials do
246
+ xml.tns(:IntegrationID, @options[:integration_id])
247
+ xml.tns(:Username, @options[:username])
248
+ xml.tns(:Password, @options[:password])
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ def build_get_account_info_request
255
+ build_header do |xml|
256
+ xml.tns :GetAccountInfo do
257
+ xml.tns(:Authenticator, authenticator)
258
+ end
259
+ end
260
+ end
261
+
262
+ def build_purchase_postage_request(purchase_amount, control_total)
263
+ build_header do |xml|
264
+ xml.tns :PurchasePostage do
265
+ xml.tns(:Authenticator, authenticator)
266
+ xml.tns(:PurchaseAmount, purchase_amount)
267
+ xml.tns(:ControlTotal, control_total)
268
+ end
269
+ end
270
+ end
271
+
272
+ def build_get_purchase_status(transaction_id)
273
+ build_header do |xml|
274
+ xml.tns :GetPurchaseStatus do
275
+ xml.tns(:Authenticator, authenticator)
276
+ xml.tns(:TransactionID, transaction_id)
277
+ end
278
+ end
279
+ end
280
+
281
+ def build_cleanse_address_request(address)
282
+ build_header do |xml|
283
+ xml.tns :CleanseAddress do
284
+ xml.tns(:Authenticator, authenticator)
285
+ add_address(xml, address)
286
+ end
287
+ end
288
+ end
289
+
290
+ def add_address(xml, address, object_type = :Address)
291
+ xml.tns object_type do
292
+ xml.tns(:FullName, address.name) unless address.name.blank?
293
+ xml.tns(:Company, address.company) unless address.company.blank?
294
+ xml.tns(:Address1, address.address1)
295
+ xml.tns(:Address2, address.address2) unless address.address2.blank?
296
+ xml.tns(:Address3, address.address3) unless address.address3.blank?
297
+ xml.tns(:City, address.city) unless address.city.blank?
298
+ if domestic?(address)
299
+ xml.tns(:State, address.state) unless address.state.blank?
300
+
301
+ zip = (address.postal_code || '').match(/^(\d{5})?-?(\d{4})?$/)
302
+ xml.tns(:ZIPCode, zip[1]) unless zip[1].nil?
303
+ xml.tns(:ZIPCodeAddOn, zip[2]) unless zip[2].nil?
304
+ else
305
+ xml.tns(:Province, address.province) unless address.province.blank?
306
+ xml.tns(:PostalCode, address.postal_code) unless address.postal_code.blank?
307
+ end
308
+ xml.tns(:Country, address.country_code) unless address.country_code.blank?
309
+ xml.tns(:PhoneNumber, address.phone) unless address.phone.blank?
310
+ end
311
+ end
312
+
313
+ def build_rate_request(origin, destination, package, options)
314
+ build_header do |xml|
315
+ xml.tns :GetRates do
316
+ xml.tns(:Authenticator, authenticator)
317
+ add_rate(xml, origin, destination, package, options)
318
+ end
319
+ end
320
+ end
321
+
322
+ def add_rate(xml, origin, destination, package, options)
323
+ value = package.value ? '%.2f' % (package.value.to_f / 100) : nil
324
+ options[:insured_value] ||= value
325
+ options[:declared_value] ||= value if international?(destination)
326
+
327
+ xml.tns :Rate do
328
+ xml.tns(:FromZIPCode, origin.postal_code) unless origin.postal_code.blank?
329
+ xml.tns(:ToZIPCode, destination.postal_code) unless destination.postal_code.blank?
330
+ xml.tns(:ToCountry, destination.country_code) unless destination.country_code.blank?
331
+ xml.tns(:ServiceType, options[:service]) unless options[:service].blank?
332
+ xml.tns(:PrintLayout, options[:print_layout]) unless options[:print_layout].blank?
333
+ xml.tns(:WeightOz, [package.ounces, 1].max)
334
+ xml.tns(:PackageType, options[:package_type] || 'Package')
335
+ xml.tns(:Length, package.inches(:length)) if package.inches(:length)
336
+ xml.tns(:Width, package.inches(:width)) if package.inches(:width)
337
+ xml.tns(:Height, package.inches(:height)) if package.inches(:height)
338
+ xml.tns(:ShipDate, options[:ship_date] || Date.today)
339
+ xml.tns(:InsuredValue, options[:insured_value]) unless options[:insured_value].blank?
340
+ xml.tns(:CODValue, options[:cod_value]) unless options[:cod_value].blank?
341
+ xml.tns(:DeclaredValue, options[:declared_value]) unless options[:declared_value].blank?
342
+
343
+ machinable = if package.options.has_key?(:machinable)
344
+ package.options[:machinable] ? true : false
345
+ else
346
+ USPS.package_machinable?(package)
347
+ end
348
+
349
+ xml.tns(:NonMachinable, true) unless machinable
350
+
351
+ xml.tns(:RectangularShaped, !package.cylinder?)
352
+ xml.tns(:GEMNotes, options[:gem_notes]) unless options[:gem_notes].blank?
353
+
354
+ add_ons = Array(options[:add_ons])
355
+ unless add_ons.empty?
356
+ xml.tns(:AddOns) do
357
+ add_ons.each do |add_on|
358
+ xml.tns(:AddOnV5) do
359
+ xml.tns(:AddOnType, add_on)
360
+ end
361
+ end
362
+ end
363
+ end
364
+
365
+ xml.tns(:ToState, destination.province) unless destination.province.blank?
366
+ end
367
+ end
368
+
369
+ def build_create_indicium_request(origin, destination, package, line_items, options)
370
+ build_header do |xml|
371
+ xml.tns :CreateIndicium do
372
+ xml.tns(:Authenticator, authenticator)
373
+ xml.tns(:IntegratorTxID, options[:integrator_tx_id] || SecureRandom::uuid)
374
+
375
+ add_rate(xml, origin, destination, package, options)
376
+ add_address(xml, origin, :From)
377
+ add_address(xml, destination, :To)
378
+ add_customs(xml, line_items, options) unless options[:content_type].blank?
379
+
380
+ xml.tns(:SampleOnly, options[:sample_only]) unless options[:sample_only].blank?
381
+ xml.tns(:ImageType, options[:image_type]) unless options[:image_type].blank?
382
+ xml.tns(:EltronPrinterDPIType, options[:label_resolution]) unless options[:label_resolution].blank?
383
+ xml.tns(:memo, options[:memo]) unless options[:memo].blank?
384
+ xml.tns(:deliveryNotification, options[:delivery_notification]) unless options[:delivery_notification].blank?
385
+
386
+ add_shipment_notification(xml, options) unless options[:email].blank?
387
+
388
+ xml.tns(:horizontalOffset, options[:horizontal_offset]) unless options[:horizontal_offest].blank?
389
+ xml.tns(:verticalOffset, options[:vertical_offset]) unless options[:vertical_offest].blank?
390
+ xml.tns(:printDensity, options[:print_density]) unless options[:print_density].blank?
391
+ xml.tns(:rotationDegrees, options[:rotation]) unless options[:rotation].blank?
392
+ xml.tns(:printMemo, options[:print_memo]) unless options[:print_memo].blank?
393
+ xml.tns(:printInstructions, options[:print_instructions]) unless options[:print_instructions].blank?
394
+ xml.tns(:ReturnImageData, options[:return_image_data]) unless options[:return_image_data].blank?
395
+ xml.tns(:InternalTransactionNumber, options[:internal_transaction_number]) unless options[:internal_transaction_number].blank?
396
+ xml.tns(:PaperSize, options[:paper_size]) unless options[:paper_size].blank?
397
+
398
+ add_label_recipient_info(xml, options) unless options[:label_email_address].blank?
399
+ end
400
+ end
401
+ end
402
+
403
+ def add_shipment_notification(xml, options)
404
+ xml.tns :ShipmentNotification do
405
+ xml.tns(:Email, options[:email])
406
+ xml.tns(:CCToAccountHolder, options[:cc_to_account_holder]) unless options[:cc_to_account_holder].blank?
407
+ xml.tns(:UseCompanyNameInFromLine, options[:use_company_name_in_from_name]) unless options[:use_company_name_in_from_line].blank?
408
+ xml.tns(:UseCompanyNameInSubject, options[:use_company_name_in_subject]) unless options[:use_company_name_in_subject].blank?
409
+ end
410
+ end
411
+
412
+ def add_customs(xml, line_items, options)
413
+ xml.tns :Customs do
414
+ xml.tns(:ContentType, options[:content_type])
415
+ xml.tns(:Comments, options[:comments]) unless options[:comments].blank?
416
+ xml.tns(:LicenseNumber, options[:license_number]) unless options[:license_number].blank?
417
+ xml.tns(:CertificateNumber, options[:certificate_number]) unless options[:certificate_number].blank?
418
+ xml.tns(:InvoiceNumber, options[:invoice_number]) unless options[:invoice_number].blank?
419
+ xml.tns(:OtherDescribe, options[:other_describe]) unless options[:other_describe].blank?
420
+
421
+ xml.tns :CustomsLines do
422
+ line_items.each do |customs_line|
423
+ xml.tns :CustomsLine do
424
+ xml.tns(:Description, customs_line.name)
425
+ xml.tns(:Quantity, customs_line.quantity)
426
+ xml.tns(:Value, '%.2f' % (customs_line.value.to_f / 100))
427
+ xml.tns(:WeightOz, customs_line.ounces) unless customs_line.ounces.blank?
428
+ xml.tns(:HSTariffNumber, customs_line.hs_code.tr('.', '')[0..5]) unless customs_line.hs_code.blank?
429
+ xml.tns(:CountryOfOrigin, customs_line.options[:country]) unless customs_line.options[:country].blank?
430
+ end
431
+ end
432
+ end
433
+ end
434
+ end
435
+
436
+ def add_label_recipient_info(xml, options)
437
+ xml.tns :LabelRecipientInfo do
438
+ xml.tns(:EmailAddress, options[:label_email_address])
439
+ xml.tns(:Name, options[:name]) unless options[:name].blank?
440
+ xml.tns(:Note, options[:note]) unless options[:note].blank?
441
+ xml.tns(:CopyToOriginator, options[:copy_to_originator]) unless options[:copy_to_originator].blank?
442
+ end
443
+ end
444
+
445
+ def build_track_shipment_request(shipment_id, options)
446
+ build_header do |xml|
447
+ xml.tns :TrackShipment do
448
+ xml.tns(:Authenticator, authenticator)
449
+ xml.tns(options[:stamps_tx_id] ? :StampsTxID : :TrackingNumber, shipment_id)
450
+ end
451
+ end
452
+ end
453
+
454
+ def commit(swsim_method, request)
455
+ save_request(request)
456
+ save_swsim_method(swsim_method)
457
+ parse(ssl_post(request_url, request, 'Content-Type' => 'text/xml', 'SOAPAction' => soap_action(swsim_method)))
458
+ rescue ActiveUtils::ResponseError => e
459
+ parse(e.response.body)
460
+ end
461
+
462
+ def request_url
463
+ test_mode? ? TEST_URL : LIVE_URL
464
+ end
465
+
466
+ def soap_action(method)
467
+ [NAMESPACE, method].join('/')
468
+ end
469
+
470
+ def parse(xml)
471
+ response_options = {}
472
+ response_options[:xml] = xml
473
+ response_options[:request] = last_request
474
+ response_options[:test] = test_mode?
475
+
476
+ document = REXML::Document.new(xml)
477
+ child_element = document.get_elements('//soap:Body/*').first
478
+ parse_method = 'parse_' + child_element.name.underscore
479
+ if respond_to?(parse_method, true)
480
+ send(parse_method, child_element, response_options)
481
+ else
482
+ Response.new(false, "Unknown response object #{child_element.name}", response_options)
483
+ end
484
+ end
485
+
486
+ def parse_fault(fault, response_options)
487
+ @authenticator = fault.get_text('detail/authenticator').value if fault.get_text('detail/authenticator')
488
+
489
+ error_code = if fault.elements['detail/stamps_exception']
490
+ fault.elements['detail/stamps_exception'].attributes['code']
491
+ elsif fault.elements['detail/sdcerror']
492
+ fault.elements['detail/sdcerror'].attributes['code']
493
+ else
494
+ nil
495
+ end
496
+
497
+ # Renew the Authenticator if it has expired and retry the request
498
+ if error_code and error_code.downcase == '002b0202'
499
+ request = renew_authenticator(last_request)
500
+ commit(last_swsim_method, request)
501
+ else
502
+ raise ResponseError.new(fault.get_text('faultstring').to_s)
503
+ end
504
+ end
505
+
506
+ def parse_authenticate_user_response(authenticate_user, response_options)
507
+ parse_authenticator(authenticate_user)
508
+ end
509
+
510
+ def parse_authenticator(response)
511
+ @authenticator = response.get_text('Authenticator').value
512
+ end
513
+
514
+ def parse_get_account_info_response(account_info_response, response_options)
515
+ parse_authenticator(account_info_response)
516
+
517
+ account_info = account_info_response.elements['AccountInfo']
518
+ response_options[:customer_id] = account_info.get_text('CustomerID').to_s
519
+ response_options[:meter_number] = account_info.get_text('MeterNumber').to_s
520
+ response_options[:user_id] = account_info.get_text('UserID').to_s
521
+ response_options[:max_postage_balance] = account_info.get_text('MaxPostageBalance').to_s
522
+ response_options[:lpo_city] = account_info.get_text('LPOCity').to_s
523
+ response_options[:lpo_state] = account_info.get_text('LPOState').to_s
524
+ response_options[:lpo_zip] = account_info.get_text('LPOZip').to_s
525
+
526
+ postage_balance_node = account_info.elements['PostageBalance']
527
+ response_options[:available_postage] = postage_balance_node.get_text('AvailablePostage').to_s
528
+ response_options[:control_total] = postage_balance_node.get_text('ControlTotal').to_s
529
+
530
+ capabilities_node = account_info.elements['Capabilities']
531
+ response_options[:can_print_shipping] = capabilities_node.get_text('CanPrintShipping').to_s == 'true'
532
+ response_options[:can_use_cost_codes] = capabilities_node.get_text('CanUseCostCodes').to_s == 'true'
533
+ response_options[:can_use_hidden_postage] = capabilities_node.get_text('CanUseHiddenPostage').to_s == 'true'
534
+ response_options[:can_purchase_sdc_insurance] = capabilities_node.get_text('CanPurchaseSDCInsurance').to_s == 'true'
535
+ response_options[:can_print_memo] = capabilities_node.get_text('CanPrintMemoOnShippingLabel').to_s == 'true'
536
+ response_options[:can_print_international] = capabilities_node.get_text('CanPrintInternational').to_s == 'true'
537
+ response_options[:can_purchase_postage] = capabilities_node.get_text('CanPurchasePostage').to_s == 'true'
538
+ response_options[:can_edit_cost_codes] = capabilities_node.get_text('CanEditCostCodes').to_s == 'true'
539
+ response_options[:must_use_cost_codes] = capabilities_node.get_text('MustUseCostCodes').to_s == 'true'
540
+ response_options[:can_view_online_reports] = capabilities_node.get_text('CanViewOnlineReports').to_s == 'true'
541
+ response_options[:per_print_limit] = capabilities_node.get_text('PerPrintLimit').to_s
542
+
543
+ StampsAccountInfoResponse.new(true, '', {}, response_options)
544
+ end
545
+
546
+ def parse_purchase_postage_response(postage, response_options)
547
+ parse_authenticator(postage)
548
+
549
+ response_options[:purchase_status] = postage.get_text('PurchaseStatus').to_s
550
+ response_options[:rejection_reason] = postage.get_text('RejectionReason').to_s if postage.get_text('RejectionReason')
551
+ response_options[:transaction_id] = postage.get_text('TransactionID').to_s if postage.get_text('TransactionID')
552
+
553
+ balance = postage.elements['PostageBalance']
554
+ response_options[:available_postage] = balance.get_text('AvailablePostage').to_s
555
+ response_options[:control_total] = balance.get_text('ControlTotal').to_s if balance.get_text('ControlTotal')
556
+
557
+ StampsPurchasePostageResponse.new(true, '', {}, response_options)
558
+ end
559
+ alias_method :parse_get_purchase_status_response, :parse_purchase_postage_response
560
+
561
+ def parse_cleanse_address_response(cleanse_address, response_options)
562
+ parse_authenticator(cleanse_address)
563
+
564
+ response_options[:address_match] = cleanse_address.get_text('AddressMatch').to_s == 'true'
565
+ response_options[:city_state_zip_ok] = cleanse_address.get_text('CityStateZipOK').to_s == 'true'
566
+
567
+ address = cleanse_address.elements['Address']
568
+ response_options[:cleanse_hash] = address.get_text('CleanseHash').to_s if address.get_text('CleanseHash')
569
+ response_options[:override_hash] = address.get_text('OverrideHash').to_s if address.get_text('OverrideHash')
570
+
571
+ address_node = cleanse_address.elements['Address']
572
+ indicator_node = cleanse_address.get_text('ResidentialDeliveryIndicatorType').to_s
573
+ po_box_node = cleanse_address.get_text('IsPOBox').to_s
574
+ response_options[:address] = parse_address(address_node, indicator_node, po_box_node)
575
+
576
+ candidate_addresses = cleanse_address.get_elements('CandidateAddresses/Address')
577
+ response_options[:candidate_addresses] = candidate_addresses.map do |candidate_address|
578
+ parse_address(candidate_address)
579
+ end
580
+
581
+ StampsCleanseAddressResponse.new(true, '', {}, response_options)
582
+ end
583
+
584
+ def parse_address(address_node, residential_indicator_node = nil, po_box_node = nil)
585
+ address = {}
586
+
587
+ address[:name] = address_node.get_text('FullName').to_s if address_node.get_text('FullName')
588
+ address[:company] = address_node.get_text('Company').to_s if address_node.get_text('Company')
589
+ address[:address1] = address_node.get_text('Address1').to_s if address_node.get_text('Address1')
590
+ address[:address2] = address_node.get_text('Address2').to_s if address_node.get_text('Address2')
591
+ address[:address3] = address_node.get_text('Address3').to_s if address_node.get_text('Address3')
592
+ address[:city] = address_node.get_text('City').to_s if address_node.get_text('City')
593
+ address[:country] = address_node.get_text('Country').to_s if address_node.get_text('Country')
594
+ address[:phone] = address_node.get_text('PhoneNumber').to_s if address_node.get_text('PhoneNumber')
595
+
596
+ if address[:country] == 'US' || address[:country].nil?
597
+ address[:state] = address_node.get_text('State').to_s if address_node.get_text('State')
598
+
599
+ address[:postal_code] = address_node.get_text('ZIPCode').to_s if address_node.get_text('ZIPCode')
600
+ address[:postal_code] += '-' + address_node.get_text('ZIPCodeAddOn').to_s if address_node.get_text('ZIPCodeAddOn')
601
+ else
602
+ address[:province] = address_node.get_text('Province').to_s if address_node.get_text('Province')
603
+ address[:postal_code] = address_node.get_text('PostalCode').to_s if address_node.get_text('PostalCode')
604
+ end
605
+
606
+ address[:address_type] = if residential_indicator_node == 'Yes'
607
+ 'residential'
608
+ elsif residential_indicator_node == 'No'
609
+ 'commercial'
610
+ elsif po_box_node == 'true'
611
+ 'po_box'
612
+ else
613
+ nil
614
+ end
615
+
616
+ Location.new(address)
617
+ end
618
+
619
+ def parse_get_rates_response(get_rates, response_options)
620
+ parse_authenticator(get_rates)
621
+
622
+ response_options[:estimates] = get_rates.get_elements('Rates/Rate').map do |rate|
623
+ parse_rate(rate)
624
+ end
625
+
626
+ RateResponse.new(true, '', {}, response_options)
627
+ end
628
+
629
+ def parse_rate(rate)
630
+ rate_options = {}
631
+
632
+ origin = Location.new(zip: rate.get_text('FromZIPCode').to_s)
633
+
634
+ location_values = {}
635
+ location_values[:zip] = rate.get_text('ToZIPCode').to_s if rate.get_text('ToZIPCode')
636
+ location_values[:country] = rate.get_text('ToCountry').to_s if rate.get_text('ToCountry')
637
+ destination = Location.new(location_values)
638
+
639
+ service_name = SERVICE_TYPES[rate.get_text('ServiceType').to_s]
640
+
641
+ rate_options[:service_code] = rate.get_text('ServiceType').to_s
642
+ rate_options[:currency] = 'USD'
643
+ rate_options[:shipping_date] = Date.parse(rate.get_text('ShipDate').to_s)
644
+
645
+ if delivery_days = rate.get_text('DeliverDays')
646
+ delivery_days = delivery_days.to_s.split('-')
647
+ rate_options[:delivery_range] = delivery_days.map { |day| rate_options[:shipping_date] + day.to_i.days }
648
+ end
649
+
650
+ rate_options[:total_price] = rate.get_text('Amount').to_s
651
+
652
+ rate_options[:add_ons] = parse_add_ons(rate)
653
+ rate_options[:packages] = parse_package(rate)
654
+
655
+ add_ons = rate_options[:add_ons]
656
+ if add_ons['SC-A-INS'] and add_ons['SC-A-INS'][:amount]
657
+ rate_options[:insurance_price] = add_ons['SC-A-INS'][:amount]
658
+ elsif add_ons['US-A-INS'] and add_ons['US-A-INS'][:amount]
659
+ rate_options[:insurance_price] = add_ons['US-A-INS'][:amount]
660
+ end
661
+
662
+ StampsRateEstimate.new(origin, destination, @@name, service_name, rate_options)
663
+ end
664
+
665
+ def parse_add_ons(rate)
666
+ add_ons = {}
667
+ rate.get_elements('AddOns/AddOnV5').each do |add_on|
668
+ add_on_type = add_on.get_text('AddOnType').to_s
669
+
670
+ add_on_details = {}
671
+ add_on_details[:missing_data] = add_on.get_text('MissingData').to_s if add_on.get_text('MissingData')
672
+ add_on_details[:amount] = add_on.get_text('Amount').to_s if add_on.get_text('Amount')
673
+
674
+ prohibited_with = add_on.get_elements('ProhibitedWithAnyOf/AddOnTypeV5').map(&:text)
675
+ add_on_details[:prohibited_with] = prohibited_with unless prohibited_with.empty?
676
+
677
+ add_ons[add_on_type] = add_on_details
678
+ end
679
+
680
+ add_ons
681
+ end
682
+
683
+ def parse_package(rate)
684
+ weight = rate.get_text('WeightOz').to_s.to_f
685
+
686
+ dimensions = %w(Length Width Height).map do |dim|
687
+ rate.get_text(dim) ? rate.get_text(dim).to_s.to_f : nil
688
+ end
689
+ dimensions.compact!
690
+
691
+ package_options = { units: :imperial }
692
+
693
+ if value = rate.get_text('InsuredValue') || rate.get_text('DeclaredValue')
694
+ package_options[:value] = value.to_s.to_f
695
+ package_options[:currency] = 'USD'
696
+ end
697
+
698
+ Package.new(weight, dimensions, package_options)
699
+ end
700
+
701
+ def parse_create_indicium_response(indicium, response_options)
702
+ parse_authenticator(indicium)
703
+
704
+ response_options[:shipping_id] = indicium.get_text('IntegratorTxID').to_s
705
+ response_options[:tracking_number] = indicium.get_text('TrackingNumber').to_s if indicium.get_text('TrackingNumber')
706
+ response_options[:stamps_tx_id] = indicium.get_text('StampsTxID').to_s
707
+ response_options[:label_url] = indicium.get_text('URL').to_s if indicium.get_text('URL')
708
+ response_options[:available_postage] = indicium.get_text('PostageBalance/AvailablePostage').to_s
709
+ response_options[:control_total] = indicium.get_text('PostageBalance/ControlTotal').to_s
710
+ response_options[:image_data] = Base64.decode64(indicium.get_text('ImageData/base64Binary').to_s) if indicium.get_text('ImageData/base64Binary')
711
+ response_options[:rate] = parse_rate(indicium.elements['Rate'])
712
+
713
+ StampsShippingResponse.new(true, '', {}, response_options)
714
+ end
715
+
716
+ def parse_track_shipment_response(track_shipment, response_options)
717
+ parse_authenticator(track_shipment)
718
+
719
+ response_options[:carrier] = @@name
720
+
721
+ shipment_events = track_shipment.get_elements('TrackingEvents/TrackingEvent').map do |event|
722
+ unless response_options[:status]
723
+ response_options[:status_code] = event.get_text('TrackingEventType').to_s
724
+ response_options[:status] = response_options[:status_code].underscore.to_sym
725
+ end
726
+
727
+ response_options[:delivery_signature] = event.get_text('SignedBy').to_s if event.get_text('SignedBy')
728
+
729
+ description = event.get_text('Event').to_s
730
+
731
+ timestamp = event.get_text('Timestamp').to_s
732
+ date, time = timestamp.split('T')
733
+ year, month, day = date.split('-')
734
+ hour, minute, second = time.split(':')
735
+ zoneless_time = Time.utc(year, month, day, hour, minute, second)
736
+
737
+ location = Location.new(
738
+ city: event.get_text('City').to_s,
739
+ state: event.get_text('State').to_s,
740
+ zip: event.get_text('Zip').to_s,
741
+ country: event.get_text('Country').to_s
742
+ )
743
+
744
+ ShipmentEvent.new(description, zoneless_time, location)
745
+ end
746
+
747
+ response_options[:shipment_events] = shipment_events.sort_by(&:time)
748
+ response_options[:delivered] = response_options[:status] == :delivered
749
+
750
+ TrackingResponse.new(true, '', {}, response_options)
751
+ end
752
+ end
753
+
754
+ class StampsAccountInfoResponse < Response
755
+ attr_reader :customer_id, :meter_number, :user_id, :available_postage, :control_total, :max_postage_balance, :lpo
756
+ attr_reader :can_print_shipping, :can_use_cost_codes, :can_use_hidden_postage, :can_purchase_sdc_insurance, :can_print_international
757
+ attr_reader :can_print_memo, :can_purchase_postage, :can_edit_cost_codes, :must_use_cost_codes, :can_view_online_reports, :per_print_limit
758
+
759
+ alias_method :can_print_shipping?, :can_print_shipping
760
+ alias_method :can_use_cost_codes?, :can_use_cost_codes
761
+ alias_method :can_use_hidden_postage?, :can_use_hidden_postage
762
+ alias_method :can_purchase_sdc_insurance?, :can_purchase_sdc_insurance
763
+ alias_method :can_print_international?, :can_print_international
764
+ alias_method :can_print_memo?, :can_print_memo
765
+ alias_method :can_purchase_postage?, :can_purchase_postage
766
+ alias_method :can_edit_cost_codes?, :can_edit_cost_codes
767
+ alias_method :must_use_cost_codes?, :must_use_cost_codes
768
+ alias_method :can_view_online_reports?, :can_view_online_reports
769
+
770
+ def initialize(success, message, params = {}, options = {})
771
+ super
772
+ @customer_id = options[:customer_id]
773
+ @meter_number = options[:meter_number]
774
+ @user_id = options[:user_id]
775
+ @available_postage = options[:available_postage]
776
+ @control_total = options[:control_total]
777
+ @max_postage_balance = options[:max_postage_balance]
778
+ @lpo = Location.new(
779
+ city: options[:lpo_city],
780
+ state: options[:lpo_state],
781
+ zip: options[:lpo_zip]
782
+ )
783
+ @can_print_shipping = options[:can_print_shipping]
784
+ @can_use_cost_codes = options[:can_use_cost_codes]
785
+ @can_use_hidden_postage = options[:can_use_hidden_postage]
786
+ @can_purchase_sdc_insurance = options[:can_purchase_sdc_insurance]
787
+ @can_print_memo = options[:can_print_memo]
788
+ @can_print_international = options[:can_print_international]
789
+ @can_purchase_postage = options[:can_purchase_postage]
790
+ @can_edit_cost_codes = options[:can_edit_cost_codes]
791
+ @must_use_cost_codes = options[:must_use_cost_codes]
792
+ @can_view_online_reports = options[:can_view_online_reports]
793
+ @per_print_limit = options[:per_print_limit]
794
+ end
795
+ end
796
+
797
+ class StampsPurchasePostageResponse < Response
798
+ attr_reader :purchase_status, :transaction_id, :available_postage, :control_total, :rejection_reason
799
+
800
+ def initialize(success, message, params = {}, options = {})
801
+ super
802
+ @purchase_status = options[:purchase_status]
803
+ @transaction_id = options[:transaction_id]
804
+ @available_postage = options[:available_postage]
805
+ @control_total = options[:control_total]
806
+ @rejection_reason = options[:rejection_reason]
807
+ end
808
+ end
809
+
810
+ class StampsCleanseAddressResponse < Response
811
+ attr_reader :address, :address_match, :city_state_zip_ok, :candidate_addresses, :cleanse_hash, :override_hash
812
+
813
+ alias_method :address_match?, :address_match
814
+ alias_method :city_state_zip_ok?, :city_state_zip_ok
815
+
816
+ def initialize(success, message, params = {}, options = {})
817
+ super
818
+ @address = options[:address]
819
+ @address_match = options[:address_match]
820
+ @city_state_zip_ok = options[:city_state_zip_ok]
821
+ @candidate_addresses = options[:candidate_addresses]
822
+ @cleanse_hash = options[:cleanse_hash]
823
+ @override_hash = options[:override_hash]
824
+ end
825
+ end
826
+
827
+ class StampsRateEstimate < RateEstimate
828
+ attr_reader :add_ons
829
+
830
+ def initialize(origin, destination, carrier, service_name, options = {})
831
+ super
832
+ @add_ons = options[:add_ons]
833
+ end
834
+
835
+ def available_add_ons
836
+ add_ons.keys
837
+ end
838
+ end
839
+
840
+ class StampsShippingResponse < ShippingResponse
841
+ include ActiveUtils::PostsData
842
+
843
+ self.ssl_version = :SSLv3
844
+
845
+ attr_reader :rate, :stamps_tx_id, :label_url, :available_postage, :control_total
846
+
847
+ def initialize(success, message, params = {}, options = {})
848
+ super
849
+ @rate = options[:rate]
850
+ @stamps_tx_id = options[:stamps_tx_id]
851
+ @label_url = options[:label_url]
852
+ @image_data = options[:image_data]
853
+ @available_postage = options[:available_postage]
854
+ @control_total = options[:control_total]
855
+ end
856
+
857
+ def image
858
+ @image_data ||= ssl_get(label_url)
859
+ end
860
+ end
861
+ end