active_shipping 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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