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