active_shipping 0.12.6 → 1.0.0.pre1

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 (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