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,26 @@
1
+ module ReactiveShipping #:nodoc:
2
+
3
+ class ExternalReturnLabelResponse < Response
4
+ attr_reader :carrier # symbol
5
+ attr_reader :carrier_name # string
6
+ attr_reader :tracking_number # string
7
+ attr_reader :return_label # string
8
+ attr_reader :postal_routing # string
9
+
10
+ def initialize(success, message, params = {}, options = {})
11
+ @carrier = options[:carrier].parameterize.to_sym
12
+ @carrier_name = options[:carrier]
13
+ @return_label = options[:return_label]
14
+ @tracking_number = options[:tracking_number]
15
+ @postal_routing = options[:postal_routing]
16
+ super
17
+ end
18
+
19
+ def has_exception?
20
+ @status == :exception
21
+ end
22
+
23
+ alias_method(:exception?, :has_exception?)
24
+ end
25
+
26
+ end
@@ -0,0 +1,10 @@
1
+ module ReactiveShipping
2
+ class Label
3
+ attr_reader :tracking_number, :img_data
4
+
5
+ def initialize(tracking_number, img_data)
6
+ @tracking_number = tracking_number
7
+ @img_data = img_data
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module ReactiveShipping
2
+ class LabelResponse < Response
3
+ attr_reader :labels
4
+
5
+ def initialize(success, message, params = {}, options = {})
6
+ @labels = options[:labels]
7
+ super
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,166 @@
1
+ module ReactiveShipping #:nodoc:
2
+ class Location
3
+ ADDRESS_TYPES = %w(residential commercial po_box)
4
+
5
+ ATTRIBUTE_ALIASES = {
6
+ name: [:name],
7
+ country: [:country_code, :country],
8
+ postal_code: [:postal_code, :zip, :postal],
9
+ province: [:province_code, :state_code, :territory_code, :region_code, :province, :state, :territory, :region],
10
+ city: [:city, :town],
11
+ address1: [:address1, :address, :street],
12
+ address2: [:address2],
13
+ address3: [:address3],
14
+ phone: [:phone, :phone_number],
15
+ fax: [:fax, :fax_number],
16
+ email: [:email],
17
+ address_type: [:address_type],
18
+ company_name: [:company, :company_name],
19
+ }.freeze
20
+
21
+ attr_reader :options,
22
+ :country,
23
+ :postal_code,
24
+ :province,
25
+ :city,
26
+ :name,
27
+ :address1,
28
+ :address2,
29
+ :address3,
30
+ :phone,
31
+ :fax,
32
+ :email,
33
+ :address_type,
34
+ :company_name
35
+
36
+ alias_method :zip, :postal_code
37
+ alias_method :postal, :postal_code
38
+ alias_method :state, :province
39
+ alias_method :territory, :province
40
+ alias_method :region, :province
41
+ alias_method :company, :company_name
42
+
43
+ def initialize(options = {})
44
+ @country = if options[:country].nil? || options[:country].is_a?(ActiveUtils::Country)
45
+ options[:country]
46
+ else
47
+ ActiveUtils::Country.find(options[:country])
48
+ end
49
+
50
+ @postal_code = options[:postal_code] || options[:postal] || options[:zip]
51
+ @province = options[:province] || options[:state] || options[:territory] || options[:region]
52
+ @city = options[:city]
53
+ @name = options[:name]
54
+ @address1 = options[:address1]
55
+ @address2 = options[:address2]
56
+ @address3 = options[:address3]
57
+ @phone = options[:phone]
58
+ @fax = options[:fax]
59
+ @email = options[:email]
60
+ @company_name = options[:company_name] || options[:company]
61
+
62
+ self.address_type = options[:address_type]
63
+ end
64
+
65
+ def self.from(object, options = {})
66
+ return object if object.is_a?(ReactiveShipping::Location)
67
+
68
+ attributes = {}
69
+
70
+ hash_access = object.respond_to?(:[])
71
+
72
+ ATTRIBUTE_ALIASES.each do |attribute, aliases|
73
+ aliases.detect do |sym|
74
+ value = object[sym] if hash_access
75
+ if !value &&
76
+ object.respond_to?(sym) &&
77
+ (!hash_access || !Hash.public_instance_methods.include?(sym))
78
+ value = object.send(sym)
79
+ end
80
+
81
+ attributes[attribute] = value if value
82
+ end
83
+ end
84
+
85
+ attributes.delete(:address_type) unless ADDRESS_TYPES.include?(attributes[:address_type].to_s)
86
+
87
+ new(attributes.update(options))
88
+ end
89
+
90
+ def country_code(format = :alpha2)
91
+ @country.nil? ? nil : @country.code(format).value
92
+ end
93
+
94
+ def residential?
95
+ @address_type == 'residential'
96
+ end
97
+
98
+ def commercial?
99
+ @address_type == 'commercial'
100
+ end
101
+
102
+ def po_box?
103
+ @address_type == 'po_box'
104
+ end
105
+
106
+ def unknown?
107
+ country_code == 'ZZ'
108
+ end
109
+
110
+ def address_type=(value)
111
+ return unless value.present?
112
+ raise ArgumentError.new("address_type must be one of #{ADDRESS_TYPES.join(', ')}") unless ADDRESS_TYPES.include?(value.to_s)
113
+ @address_type = value.to_s
114
+ end
115
+
116
+ def to_hash
117
+ {
118
+ country: country_code,
119
+ postal_code: postal_code,
120
+ province: province,
121
+ city: city,
122
+ name: name,
123
+ address1: address1,
124
+ address2: address2,
125
+ address3: address3,
126
+ phone: phone,
127
+ fax: fax,
128
+ email: email,
129
+ address_type: address_type,
130
+ company_name: company_name
131
+ }
132
+ end
133
+
134
+ def to_s
135
+ prettyprint.gsub(/\n/, ' ')
136
+ end
137
+
138
+ def prettyprint
139
+ chunks = [@name, @address1, @address2, @address3]
140
+ chunks << [@city, @province, @postal_code].reject(&:blank?).join(', ')
141
+ chunks << @country
142
+ chunks.reject(&:blank?).join("\n")
143
+ end
144
+
145
+ def inspect
146
+ string = prettyprint
147
+ string << "\nPhone: #{@phone}" unless @phone.blank?
148
+ string << "\nFax: #{@fax}" unless @fax.blank?
149
+ string << "\nEmail: #{@email}" unless @email.blank?
150
+ string
151
+ end
152
+
153
+ # Returns the postal code as a properly formatted Zip+4 code, e.g. "77095-2233"
154
+ def zip_plus_4
155
+ "#{$1}-#{$2}" if /(\d{5})-?(\d{4})/ =~ @postal_code
156
+ end
157
+
158
+ def address2_and_3
159
+ [address2, address3].reject(&:blank?).join(", ")
160
+ end
161
+
162
+ def ==(other)
163
+ to_hash == other.to_hash
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,165 @@
1
+ module ReactiveShipping #:nodoc:
2
+ class Package
3
+ cattr_accessor :default_options
4
+ attr_reader :options, :value, :currency
5
+
6
+ # Package.new(100, [10, 20, 30], :units => :metric)
7
+ # Package.new(Measured::Weight.new(100, :g), [10, 20, 30].map {|m| Length.new(m, :centimetres)})
8
+ # Package.new(100.grams, [10, 20, 30].map(&:centimetres))
9
+ def initialize(grams_or_ounces, dimensions, options = {})
10
+ options = @@default_options.update(options) if @@default_options
11
+ options.symbolize_keys!
12
+ @options = options
13
+
14
+ @dimensions = [dimensions].flatten.reject(&:nil?)
15
+
16
+ imperial = (options[:units] == :imperial)
17
+
18
+ weight_imperial = dimensions_imperial = imperial if options.include?(:units)
19
+
20
+ if options.include?(:weight_units)
21
+ weight_imperial = (options[:weight_units] == :imperial)
22
+ end
23
+
24
+ if options.include?(:dim_units)
25
+ dimensions_imperial = (options[:dim_units] == :imperial)
26
+ end
27
+
28
+ @weight_unit_system = weight_imperial ? :imperial : :metric
29
+ @dimensions_unit_system = dimensions_imperial ? :imperial : :metric
30
+
31
+ @weight = attribute_from_metric_or_imperial(grams_or_ounces, Measured::Weight, @weight_unit_system, :grams, :ounces)
32
+
33
+ if @dimensions.blank?
34
+ zero_length = Measured::Length.new(0, (dimensions_imperial ? :inches : :centimetres))
35
+ @dimensions = [zero_length] * 3
36
+ else
37
+ process_dimensions
38
+ end
39
+
40
+ @value = Package.cents_from(options[:value])
41
+ @currency = options[:currency] || (options[:value].currency if options[:value].respond_to?(:currency))
42
+ @cylinder = (options[:cylinder] || options[:tube]) ? true : false
43
+ @gift = options[:gift] ? true : false
44
+ @oversized = options[:oversized] ? true : false
45
+ @unpackaged = options[:unpackaged] ? true : false
46
+ end
47
+
48
+ def unpackaged?
49
+ @unpackaged
50
+ end
51
+
52
+ def oversized?
53
+ @oversized
54
+ end
55
+
56
+ def cylinder?
57
+ @cylinder
58
+ end
59
+ alias_method :tube?, :cylinder?
60
+
61
+ def gift?
62
+ @gift
63
+ end
64
+
65
+ def ounces(options = {})
66
+ weight(options).convert_to(:oz).value.to_f
67
+ end
68
+ alias_method :oz, :ounces
69
+
70
+ def grams(options = {})
71
+ weight(options).convert_to(:g).value.to_f
72
+ end
73
+ alias_method :g, :grams
74
+
75
+ def pounds(options = {})
76
+ weight(options).convert_to(:lb).value.to_f
77
+ end
78
+ alias_method :lb, :pounds
79
+ alias_method :lbs, :pounds
80
+
81
+ def kilograms(options = {})
82
+ weight(options).convert_to(:kg).value.to_f
83
+ end
84
+ alias_method :kg, :kilograms
85
+ alias_method :kgs, :kilograms
86
+
87
+ def inches(measurement = nil)
88
+ @inches ||= @dimensions.map { |m| m.convert_to(:in).value.to_f }
89
+ measurement.nil? ? @inches : measure(measurement, @inches)
90
+ end
91
+ alias_method :in, :inches
92
+
93
+ def centimetres(measurement = nil)
94
+ @centimetres ||= @dimensions.map { |m| m.convert_to(:cm).value.to_f }
95
+ measurement.nil? ? @centimetres : measure(measurement, @centimetres)
96
+ end
97
+ alias_method :cm, :centimetres
98
+
99
+ def weight(options = {})
100
+ case options[:type]
101
+ when nil, :actual
102
+ @weight
103
+ when :volumetric, :dimensional
104
+ @volumetric_weight ||= begin
105
+ m = Measured::Weight.new((centimetres(:box_volume) / 6.0), :grams)
106
+ @weight_unit_system == :imperial ? m.convert_to(:oz) : m
107
+ end
108
+ when :billable
109
+ [weight, weight(:type => :volumetric)].max
110
+ end
111
+ end
112
+ alias_method :mass, :weight
113
+
114
+ def self.cents_from(money)
115
+ return nil if money.nil?
116
+ if money.respond_to?(:cents)
117
+ return money.cents
118
+ else
119
+ case money
120
+ when Float
121
+ (money * 100).round
122
+ when String
123
+ money =~ /\./ ? (money.to_f * 100).round : money.to_i
124
+ else
125
+ money.to_i
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def attribute_from_metric_or_imperial(obj, klass, unit_system, metric_unit, imperial_unit)
133
+ if obj.is_a?(klass)
134
+ return obj
135
+ else
136
+ return klass.new(obj, (unit_system == :imperial ? imperial_unit : metric_unit))
137
+ end
138
+ end
139
+
140
+ def measure(measurement, ary)
141
+ case measurement
142
+ when Integer then ary[measurement]
143
+ when :x, :max, :length, :long then ary[2]
144
+ when :y, :mid, :width, :wide then ary[1]
145
+ when :z, :min, :height, :depth, :high, :deep then ary[0]
146
+ when :girth, :around, :circumference
147
+ self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 2) : (2 * ary[0]) + (2 * ary[1])
148
+ when :volume then self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 4)**2 * ary[2] : measure(:box_volume, ary)
149
+ when :box_volume then ary[0] * ary[1] * ary[2]
150
+ end
151
+ end
152
+
153
+ def process_dimensions
154
+ @dimensions = @dimensions.map do |l|
155
+ attribute_from_metric_or_imperial(l, Measured::Length, @dimensions_unit_system, :centimetres, :inches)
156
+ end.sort
157
+ # [1,2] => [1,1,2]
158
+ # [5] => [5,5,5]
159
+ # etc..
160
+ 2.downto(@dimensions.length) do |_n|
161
+ @dimensions.unshift(@dimensions[0])
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,60 @@
1
+ module ReactiveShipping #:nodoc:
2
+ class PackageItem
3
+ attr_reader :sku, :hs_code, :value, :name, :weight, :quantity, :options
4
+
5
+ def initialize(name, grams_or_ounces, value, quantity, options = {})
6
+ @name = name
7
+
8
+ imperial = (options[:units] == :imperial)
9
+
10
+ @unit_system = imperial ? :imperial : :metric
11
+
12
+ @weight = grams_or_ounces
13
+ @weight = Measured::Weight.new(grams_or_ounces, (@unit_system == :imperial ? :oz : :g)) unless @weight.is_a?(Measured::Weight)
14
+
15
+ @value = Package.cents_from(value)
16
+ @quantity = quantity > 0 ? quantity : 1
17
+
18
+ @sku = options[:sku]
19
+ @hs_code = options[:hs_code]
20
+ @options = options
21
+ end
22
+
23
+ def weight(options = {})
24
+ case options[:type]
25
+ when nil, :actual
26
+ @weight
27
+ when :volumetric, :dimensional
28
+ @volumetric_weight ||= begin
29
+ m = Measured::Weight.new((centimetres(:box_volume) / 6.0), :grams)
30
+ @unit_system == :imperial ? m.in_ounces : m
31
+ end
32
+ when :billable
33
+ [weight, weight(:type => :volumetric)].max
34
+ end
35
+ end
36
+ alias_method :mass, :weight
37
+
38
+ def ounces(options = {})
39
+ weight(options).convert_to(:oz).value
40
+ end
41
+ alias_method :oz, :ounces
42
+
43
+ def grams(options = {})
44
+ weight(options).convert_to(:g).value
45
+ end
46
+ alias_method :g, :grams
47
+
48
+ def pounds(options = {})
49
+ weight(options).convert_to(:lb).value
50
+ end
51
+ alias_method :lb, :pounds
52
+ alias_method :lbs, :pounds
53
+
54
+ def kilograms(options = {})
55
+ weight(options).convert_to(:kg).value
56
+ end
57
+ alias_method :kg, :kilograms
58
+ alias_method :kgs, :kilograms
59
+ end
60
+ end
@@ -0,0 +1,197 @@
1
+ module ReactiveShipping
2
+
3
+ # Class representing a shipping option with estimated price.
4
+ #
5
+ # @!attribute origin
6
+ # The origin of the shipment
7
+ # @return [ReactiveShipping::Location]
8
+ #
9
+ # @!attribute destination
10
+ # The destination of the shipment
11
+ # @return [ReactiveShipping::Location]
12
+ #
13
+ # @!attribute package_rates
14
+ # A list of rates for all the packages in the shipment
15
+ # @return [Array<{:rate => Integer, :package => ReactiveShipping::Package}>]
16
+ #
17
+ # @!attribute carrier
18
+ # The name of the carrier (e.g. 'USPS', 'FedEx')
19
+ # @return [String]
20
+ # @see ReactiveShipping::Carrier.name
21
+ #
22
+ # @!attribute service_name
23
+ # The name of the shipping service (e.g. 'First Class Ground')
24
+ # @return [String]
25
+ #
26
+ # @!attribute service_code
27
+ # The code of the shipping service
28
+ # @return [String]
29
+ #
30
+ # @!attribute description
31
+ # Public description of the shipping service (e.g. '2 days delivery')
32
+ # @return [String]
33
+ #
34
+ # @!attribute shipping_date
35
+ # The date on which the shipment will be expected. Normally, this means that the
36
+ # delivery date range can only be promised if the shipment is handed over on or
37
+ # before this date.
38
+ # @return [Date]
39
+ #
40
+ # @!attribute delivery_date
41
+ # The date on which the shipment will be delivered. This is usually only available
42
+ # for express shipments; in other cases a {#delivery_range} is given instead.
43
+ # @return [Date]
44
+ #
45
+ # @!attribute delivery_range
46
+ # The minimum and maximum date of when the shipment is expected to be delivered
47
+ # @return [Array<Date>]
48
+ #
49
+ # @!attribute currency
50
+ # ISO4217 currency code of the quoted rate estimates (e.g. `CAD`, `EUR`, or `USD`)
51
+ # @return [String]
52
+ # @see http://en.wikipedia.org/wiki/ISO_4217
53
+ #
54
+ # @!attribute negotiated_rate
55
+ # The negotiated rate in cents
56
+ # @return [Integer]
57
+ #
58
+ # @!attribute compare_price
59
+ # The comparable price in cents
60
+ # @return [Integer]
61
+ #
62
+ # @!attribute phone_required
63
+ # Specifies if a phone number is required for the shipping rate
64
+ # @return [Boolean]
65
+ #
66
+ # @!attribute insurance_price
67
+ # The price of insurance in cents
68
+ # @return [Integer]
69
+ #
70
+ # @!attribute delivery_category
71
+ # The general classification of the delivery method
72
+ # @return [String]
73
+ #
74
+ # @!attribute shipment_options
75
+ # Additional priced options bundled with the given rate estimate with price in cents
76
+ # @return [Array<{ code: String, price: Integer }>]
77
+ #
78
+ # @!attribute charge_items
79
+ # Breakdown of a shipping rate's price with amounts in cents.
80
+ # @return [Array<{ group: String, code: String, name: String, description: String, amount: Integer }>]
81
+ #
82
+ class RateEstimate
83
+ attr_accessor :origin, :destination, :package_rates,
84
+ :carrier, :service_name, :service_code, :description,
85
+ :shipping_date, :delivery_date, :delivery_range,
86
+ :currency, :negotiated_rate, :insurance_price,
87
+ :estimate_reference, :expires_at, :pickup_time,
88
+ :compare_price, :phone_required, :delivery_category,
89
+ :shipment_options, :charge_items, :messages
90
+
91
+ def initialize(origin, destination, carrier, service_name, options = {})
92
+ self.origin, self.destination, self.carrier, self.service_name = origin, destination, carrier, service_name
93
+ self.service_code = options[:service_code]
94
+ self.description = options[:description]
95
+ self.estimate_reference = options[:estimate_reference]
96
+ self.pickup_time = options[:pickup_time]
97
+ self.expires_at = options[:expires_at]
98
+ if options[:package_rates]
99
+ self.package_rates = options[:package_rates].map { |p| p.update(:rate => Package.cents_from(p[:rate])) }
100
+ else
101
+ self.package_rates = Array(options[:packages]).map { |p| {:package => p} }
102
+ end
103
+ self.total_price = options[:total_price]
104
+ self.negotiated_rate = options[:negotiated_rate]
105
+ self.compare_price = options[:compare_price]
106
+ self.phone_required = options[:phone_required]
107
+ self.currency = options[:currency]
108
+ self.delivery_range = options[:delivery_range]
109
+ self.shipping_date = options[:shipping_date]
110
+ self.delivery_date = @delivery_range.last
111
+ self.insurance_price = options[:insurance_price]
112
+ self.delivery_category = options[:delivery_category]
113
+ self.shipment_options = options[:shipment_options] || []
114
+ self.charge_items = options[:charge_items] || []
115
+ self.messages = options[:messages] || []
116
+ end
117
+
118
+ # The total price of the shipments in cents.
119
+ # @return [Integer]
120
+ def total_price
121
+ @total_price || @package_rates.sum { |pr| pr[:rate] }
122
+ rescue NoMethodError
123
+ raise ArgumentError.new("RateEstimate must have a total_price set, or have a full set of valid package rates.")
124
+ end
125
+ alias_method :price, :total_price
126
+
127
+ # Adds a package to this rate estimate
128
+ # @param package [ReactiveShipping::Package] The package to add.
129
+ # @param rate [#cents, Float, String, nil] The rate for this package. This is only required if
130
+ # there is no total price for this shipment
131
+ # @return [self]
132
+ def add(package, rate = nil)
133
+ cents = Package.cents_from(rate)
134
+ raise ArgumentError.new("New packages must have valid rate information since this RateEstimate has no total_price set.") if cents.nil? and total_price.nil?
135
+ @package_rates << {:package => package, :rate => cents}
136
+ self
137
+ end
138
+
139
+ # The list of packages for which rate estimates are given.
140
+ # @return [Array<ReactiveShipping::Package>]
141
+ def packages
142
+ package_rates.map { |p| p[:package] }
143
+ end
144
+
145
+ # The number of packages for which rate estimates are given.
146
+ # @return [Integer]
147
+ def package_count
148
+ package_rates.length
149
+ end
150
+
151
+ protected
152
+
153
+ def delivery_range=(delivery_range)
154
+ @delivery_range = delivery_range ? delivery_range.map { |date| date_for(date) }.compact : []
155
+ end
156
+
157
+ def total_price=(total_price)
158
+ @total_price = Package.cents_from(total_price)
159
+ end
160
+
161
+ def negotiated_rate=(negotiated_rate)
162
+ @negotiated_rate = negotiated_rate ? Package.cents_from(negotiated_rate) : nil
163
+ end
164
+
165
+ def compare_price=(compare_price)
166
+ @compare_price = compare_price ? Package.cents_from(compare_price) : nil
167
+ end
168
+
169
+ def currency=(currency)
170
+ @currency = ActiveUtils::CurrencyCode.standardize(currency)
171
+ end
172
+
173
+ def phone_required=(phone_required)
174
+ @phone_required = !!phone_required
175
+ end
176
+
177
+ def shipping_date=(shipping_date)
178
+ @shipping_date = date_for(shipping_date)
179
+ end
180
+
181
+ def insurance_price=(insurance_price)
182
+ @insurance_price = Package.cents_from(insurance_price)
183
+ end
184
+
185
+ private
186
+
187
+ # Returns a Date object for a given input
188
+ # @param date [String, Date, Time, DateTime, ...] The object to infer a date from.
189
+ # @return [Date, nil] The Date object absed on the input, or `nil` if no date
190
+ # could be determined.
191
+ def date_for(date)
192
+ date && Date.strptime(date.to_s, "%Y-%m-%d")
193
+ rescue ArgumentError
194
+ nil
195
+ end
196
+ end
197
+ end