reactive_shipping 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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