active_shipping 0.11.2 → 0.12.0

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