active_shipping 0.12.6 → 1.0.0.pre1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/{CHANGELOG → CHANGELOG.md} +6 -2
  5. data/CONTRIBUTING.md +32 -0
  6. data/{README.markdown → README.md} +45 -61
  7. data/lib/active_shipping.rb +20 -28
  8. data/lib/active_shipping/carrier.rb +82 -0
  9. data/lib/active_shipping/carriers.rb +33 -0
  10. data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
  11. data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
  12. data/lib/active_shipping/carriers/canada_post.rb +253 -0
  13. data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
  14. data/lib/active_shipping/carriers/fedex.rb +579 -0
  15. data/lib/active_shipping/carriers/kunaki.rb +164 -0
  16. data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
  17. data/lib/active_shipping/carriers/shipwire.rb +181 -0
  18. data/lib/active_shipping/carriers/stamps.rb +861 -0
  19. data/lib/active_shipping/carriers/ups.rb +648 -0
  20. data/lib/active_shipping/carriers/usps.rb +642 -0
  21. data/lib/active_shipping/errors.rb +7 -0
  22. data/lib/active_shipping/label_response.rb +23 -0
  23. data/lib/active_shipping/location.rb +149 -0
  24. data/lib/active_shipping/package.rb +241 -0
  25. data/lib/active_shipping/rate_estimate.rb +64 -0
  26. data/lib/active_shipping/rate_response.rb +13 -0
  27. data/lib/active_shipping/response.rb +41 -0
  28. data/lib/active_shipping/shipment_event.rb +17 -0
  29. data/lib/active_shipping/shipment_packer.rb +73 -0
  30. data/lib/active_shipping/shipping_response.rb +12 -0
  31. data/lib/active_shipping/tracking_response.rb +52 -0
  32. data/lib/active_shipping/version.rb +1 -1
  33. data/lib/vendor/quantified/test/length_test.rb +2 -2
  34. data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
  35. metadata +58 -36
  36. metadata.gz.sig +0 -0
  37. data/lib/active_shipping/shipping/base.rb +0 -13
  38. data/lib/active_shipping/shipping/carrier.rb +0 -84
  39. data/lib/active_shipping/shipping/carriers.rb +0 -23
  40. data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
  41. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
  42. data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
  43. data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
  44. data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
  45. data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
  46. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
  47. data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
  48. data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
  49. data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
  50. data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
  51. data/lib/active_shipping/shipping/errors.rb +0 -9
  52. data/lib/active_shipping/shipping/label_response.rb +0 -25
  53. data/lib/active_shipping/shipping/location.rb +0 -152
  54. data/lib/active_shipping/shipping/package.rb +0 -243
  55. data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
  56. data/lib/active_shipping/shipping/rate_response.rb +0 -15
  57. data/lib/active_shipping/shipping/response.rb +0 -43
  58. data/lib/active_shipping/shipping/shipment_event.rb +0 -19
  59. data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
  60. data/lib/active_shipping/shipping/shipping_response.rb +0 -14
  61. data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -0,0 +1,7 @@
1
+ module ActiveShipping
2
+ class ResponseContentError < StandardError
3
+ def initialize(exception, content_body)
4
+ super("#{exception.message} \n\n#{content_body}")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveShipping
2
+ # This is UPS specific for now; the hash is not at all generic
3
+ # or common between carriers.
4
+
5
+ class LabelResponse < Response
6
+ attr :params # maybe?
7
+
8
+ def initialize(success, message, params = {}, options = {})
9
+ @params = params
10
+ super
11
+ end
12
+
13
+ def labels
14
+ return @labels if @labels
15
+ packages = params["ShipmentResults"]["PackageResults"]
16
+ packages = [packages] if Hash === packages
17
+ @labels = packages.map do |package|
18
+ { :tracking_number => package["TrackingNumber"],
19
+ :image => package["LabelImage"] }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,149 @@
1
+ module ActiveShipping #:nodoc:
2
+ class Location
3
+ ADDRESS_TYPES = %w(residential commercial po_box)
4
+
5
+ attr_reader :options,
6
+ :country,
7
+ :postal_code,
8
+ :province,
9
+ :city,
10
+ :name,
11
+ :address1,
12
+ :address2,
13
+ :address3,
14
+ :phone,
15
+ :fax,
16
+ :address_type,
17
+ :company_name
18
+
19
+ alias_method :zip, :postal_code
20
+ alias_method :postal, :postal_code
21
+ alias_method :state, :province
22
+ alias_method :territory, :province
23
+ alias_method :region, :province
24
+ alias_method :company, :company_name
25
+
26
+ def initialize(options = {})
27
+ @country = (options[:country].nil? or options[:country].is_a?(ActiveUtils::Country)) ?
28
+ options[:country] :
29
+ ActiveUtils::Country.find(options[:country])
30
+ @postal_code = options[:postal_code] || options[:postal] || options[:zip]
31
+ @province = options[:province] || options[:state] || options[:territory] || options[:region]
32
+ @city = options[:city]
33
+ @name = options[:name]
34
+ @address1 = options[:address1]
35
+ @address2 = options[:address2]
36
+ @address3 = options[:address3]
37
+ @phone = options[:phone]
38
+ @fax = options[:fax]
39
+ @company_name = options[:company_name] || options[:company]
40
+
41
+ self.address_type = options[:address_type]
42
+ end
43
+
44
+ def self.from(object, options = {})
45
+ return object if object.is_a? ActiveShipping::Location
46
+ attr_mappings = {
47
+ :name => [:name],
48
+ :country => [:country_code, :country],
49
+ :postal_code => [:postal_code, :zip, :postal],
50
+ :province => [:province_code, :state_code, :territory_code, :region_code, :province, :state, :territory, :region],
51
+ :city => [:city, :town],
52
+ :address1 => [:address1, :address, :street],
53
+ :address2 => [:address2],
54
+ :address3 => [:address3],
55
+ :phone => [:phone, :phone_number],
56
+ :fax => [:fax, :fax_number],
57
+ :address_type => [:address_type],
58
+ :company_name => [:company, :company_name]
59
+ }
60
+ attributes = {}
61
+ hash_access = begin
62
+ object[:some_symbol]
63
+ true
64
+ rescue
65
+ false
66
+ end
67
+ attr_mappings.each do |pair|
68
+ pair[1].each do |sym|
69
+ if value = (object[sym] if hash_access) || (object.send(sym) if object.respond_to?(sym) && (!hash_access || !Hash.public_instance_methods.include?(sym.to_s)))
70
+ attributes[pair[0]] = value
71
+ break
72
+ end
73
+ end
74
+ end
75
+ attributes.delete(:address_type) unless ADDRESS_TYPES.include?(attributes[:address_type].to_s)
76
+ new(attributes.update(options))
77
+ end
78
+
79
+ def country_code(format = :alpha2)
80
+ @country.nil? ? nil : @country.code(format).value
81
+ end
82
+
83
+ def residential?; @address_type == 'residential' end
84
+ def commercial?; @address_type == 'commercial' end
85
+ def po_box?; @address_type == 'po_box' end
86
+ def unknown?; country_code == 'ZZ' end
87
+
88
+ def address_type=(value)
89
+ return unless value.present?
90
+ raise ArgumentError.new("address_type must be one of #{ADDRESS_TYPES.join(', ')}") unless ADDRESS_TYPES.include?(value.to_s)
91
+ @address_type = value.to_s
92
+ end
93
+
94
+ def to_hash
95
+ {
96
+ :country => country_code,
97
+ :postal_code => postal_code,
98
+ :province => province,
99
+ :city => city,
100
+ :name => name,
101
+ :address1 => address1,
102
+ :address2 => address2,
103
+ :address3 => address3,
104
+ :phone => phone,
105
+ :fax => fax,
106
+ :address_type => address_type,
107
+ :company_name => company_name
108
+ }
109
+ end
110
+
111
+ def to_xml(options = {})
112
+ options[:root] ||= "location"
113
+ to_hash.to_xml(options)
114
+ end
115
+
116
+ def to_s
117
+ prettyprint.gsub(/\n/, ' ')
118
+ end
119
+
120
+ def prettyprint
121
+ chunks = [@name, @address1, @address2, @address3]
122
+ chunks << [@city, @province, @postal_code].reject(&:blank?).join(', ')
123
+ chunks << @country
124
+ chunks.reject(&:blank?).join("\n")
125
+ end
126
+
127
+ def inspect
128
+ string = prettyprint
129
+ string << "\nPhone: #{@phone}" unless @phone.blank?
130
+ string << "\nFax: #{@fax}" unless @fax.blank?
131
+ string
132
+ end
133
+
134
+ # Returns the postal code as a properly formatted Zip+4 code, e.g. "77095-2233"
135
+ def zip_plus_4
136
+ if /(\d{5})(\d{4})/ =~ @postal_code
137
+ return "#{$1}-#{$2}"
138
+ elsif /\d{5}-\d{4}/ =~ @postal_code
139
+ return @postal_code
140
+ else
141
+ nil
142
+ end
143
+ end
144
+
145
+ def address2_and_3
146
+ [address2, address3].reject(&:blank?).join(", ")
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,241 @@
1
+ module ActiveShipping #:nodoc:
2
+ # A package item is a unique item(s) that is physically in a package.
3
+ # A single package can have many items. This is only required
4
+ # for shipping methods (label creation) right now.
5
+ class PackageItem
6
+ include Quantified
7
+
8
+ attr_reader :sku, :hs_code, :value, :name, :weight, :quantity, :options
9
+
10
+ def initialize(name, grams_or_ounces, value, quantity, options = {})
11
+ @name = name
12
+
13
+ imperial = (options[:units] == :imperial) ||
14
+ (grams_or_ounces.respond_to?(:unit) && m.unit.to_sym == :imperial)
15
+
16
+ @unit_system = imperial ? :imperial : :metric
17
+
18
+ @weight = attribute_from_metric_or_imperial(grams_or_ounces, Mass, :grams, :ounces)
19
+
20
+ @value = Package.cents_from(value)
21
+ @quantity = quantity > 0 ? quantity : 1
22
+
23
+ @sku = options[:sku]
24
+ @hs_code = options[:hs_code]
25
+ @options = options
26
+ end
27
+
28
+ def weight(options = {})
29
+ case options[:type]
30
+ when nil, :actual
31
+ @weight
32
+ when :volumetric, :dimensional
33
+ @volumetric_weight ||= begin
34
+ m = Mass.new((centimetres(:box_volume) / 6.0), :grams)
35
+ @unit_system == :imperial ? m.in_ounces : m
36
+ end
37
+ when :billable
38
+ [weight, weight(:type => :volumetric)].max
39
+ end
40
+ end
41
+ alias_method :mass, :weight
42
+
43
+ def ounces(options = {})
44
+ weight(options).in_ounces.amount
45
+ end
46
+ alias_method :oz, :ounces
47
+
48
+ def grams(options = {})
49
+ weight(options).in_grams.amount
50
+ end
51
+ alias_method :g, :grams
52
+
53
+ def pounds(options = {})
54
+ weight(options).in_pounds.amount
55
+ end
56
+ alias_method :lb, :pounds
57
+ alias_method :lbs, :pounds
58
+
59
+ def kilograms(options = {})
60
+ weight(options).in_kilograms.amount
61
+ end
62
+ alias_method :kg, :kilograms
63
+ alias_method :kgs, :kilograms
64
+
65
+ private
66
+
67
+ def attribute_from_metric_or_imperial(obj, klass, metric_unit, imperial_unit)
68
+ if obj.is_a?(klass)
69
+ return value
70
+ else
71
+ return klass.new(obj, (@unit_system == :imperial ? imperial_unit : metric_unit))
72
+ end
73
+ end
74
+ end
75
+
76
+ class Package
77
+ include Quantified
78
+
79
+ cattr_accessor :default_options
80
+ attr_reader :options, :value, :currency
81
+
82
+ # Package.new(100, [10, 20, 30], :units => :metric)
83
+ # Package.new(Mass.new(100, :grams), [10, 20, 30].map {|m| Length.new(m, :centimetres)})
84
+ # Package.new(100.grams, [10, 20, 30].map(&:centimetres))
85
+ def initialize(grams_or_ounces, dimensions, options = {})
86
+ options = @@default_options.update(options) if @@default_options
87
+ options.symbolize_keys!
88
+ @options = options
89
+
90
+ @dimensions = [dimensions].flatten.reject(&:nil?)
91
+
92
+ imperial = (options[:units] == :imperial) ||
93
+ ([grams_or_ounces, *dimensions].all? { |m| m.respond_to?(:unit) && m.unit.to_sym == :imperial })
94
+
95
+ weight_imperial = dimensions_imperial = imperial if options.include?(:units)
96
+
97
+ if options.include?(:weight_units)
98
+ weight_imperial = (options[:weight_units] == :imperial) ||
99
+ (grams_or_ounces.respond_to?(:unit) && m.unit.to_sym == :imperial)
100
+ end
101
+
102
+ if options.include?(:dim_units)
103
+ dimensions_imperial = (options[:dim_units] == :imperial) ||
104
+ (dimensions && dimensions.all? { |m| m.respond_to?(:unit) && m.unit.to_sym == :imperial })
105
+ end
106
+
107
+ @weight_unit_system = weight_imperial ? :imperial : :metric
108
+ @dimensions_unit_system = dimensions_imperial ? :imperial : :metric
109
+
110
+ @weight = attribute_from_metric_or_imperial(grams_or_ounces, Mass, @weight_unit_system, :grams, :ounces)
111
+
112
+ if @dimensions.blank?
113
+ @dimensions = [Length.new(0, (dimensions_imperial ? :inches : :centimetres))] * 3
114
+ else
115
+ process_dimensions
116
+ end
117
+
118
+ @value = Package.cents_from(options[:value])
119
+ @currency = options[:currency] || (options[:value].currency if options[:value].respond_to?(:currency))
120
+ @cylinder = (options[:cylinder] || options[:tube]) ? true : false
121
+ @gift = options[:gift] ? true : false
122
+ @oversized = options[:oversized] ? true : false
123
+ @unpackaged = options[:unpackaged] ? true : false
124
+ end
125
+
126
+ def unpackaged?
127
+ @unpackaged
128
+ end
129
+
130
+ def oversized?
131
+ @oversized
132
+ end
133
+
134
+ def cylinder?
135
+ @cylinder
136
+ end
137
+ alias_method :tube?, :cylinder?
138
+
139
+ def gift?; @gift end
140
+
141
+ def ounces(options = {})
142
+ weight(options).in_ounces.amount
143
+ end
144
+ alias_method :oz, :ounces
145
+
146
+ def grams(options = {})
147
+ weight(options).in_grams.amount
148
+ end
149
+ alias_method :g, :grams
150
+
151
+ def pounds(options = {})
152
+ weight(options).in_pounds.amount
153
+ end
154
+ alias_method :lb, :pounds
155
+ alias_method :lbs, :pounds
156
+
157
+ def kilograms(options = {})
158
+ weight(options).in_kilograms.amount
159
+ end
160
+ alias_method :kg, :kilograms
161
+ alias_method :kgs, :kilograms
162
+
163
+ def inches(measurement = nil)
164
+ @inches ||= @dimensions.map { |m| m.in_inches.amount }
165
+ measurement.nil? ? @inches : measure(measurement, @inches)
166
+ end
167
+ alias_method :in, :inches
168
+
169
+ def centimetres(measurement = nil)
170
+ @centimetres ||= @dimensions.map { |m| m.in_centimetres.amount }
171
+ measurement.nil? ? @centimetres : measure(measurement, @centimetres)
172
+ end
173
+ alias_method :cm, :centimetres
174
+
175
+ def weight(options = {})
176
+ case options[:type]
177
+ when nil, :actual
178
+ @weight
179
+ when :volumetric, :dimensional
180
+ @volumetric_weight ||= begin
181
+ m = Mass.new((centimetres(:box_volume) / 6.0), :grams)
182
+ @weight_unit_system == :imperial ? m.in_ounces : m
183
+ end
184
+ when :billable
185
+ [weight, weight(:type => :volumetric)].max
186
+ end
187
+ end
188
+ alias_method :mass, :weight
189
+
190
+ def self.cents_from(money)
191
+ return nil if money.nil?
192
+ if money.respond_to?(:cents)
193
+ return money.cents
194
+ else
195
+ case money
196
+ when Float
197
+ (money * 100).round
198
+ when String
199
+ money =~ /\./ ? (money.to_f * 100).round : money.to_i
200
+ else
201
+ money.to_i
202
+ end
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def attribute_from_metric_or_imperial(obj, klass, unit_system, metric_unit, imperial_unit)
209
+ if obj.is_a?(klass)
210
+ return obj
211
+ else
212
+ return klass.new(obj, (unit_system == :imperial ? imperial_unit : metric_unit))
213
+ end
214
+ end
215
+
216
+ def measure(measurement, ary)
217
+ case measurement
218
+ when Fixnum then ary[measurement]
219
+ when :x, :max, :length, :long then ary[2]
220
+ when :y, :mid, :width, :wide then ary[1]
221
+ when :z, :min, :height, :depth, :high, :deep then ary[0]
222
+ when :girth, :around, :circumference
223
+ self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 2) : (2 * ary[0]) + (2 * ary[1])
224
+ when :volume then self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 4)**2 * ary[2] : measure(:box_volume, ary)
225
+ when :box_volume then ary[0] * ary[1] * ary[2]
226
+ end
227
+ end
228
+
229
+ def process_dimensions
230
+ @dimensions = @dimensions.map do |l|
231
+ attribute_from_metric_or_imperial(l, Length, @dimensions_unit_system, :centimetres, :inches)
232
+ end.sort
233
+ # [1,2] => [1,1,2]
234
+ # [5] => [5,5,5]
235
+ # etc..
236
+ 2.downto(@dimensions.length) do |_n|
237
+ @dimensions.unshift(@dimensions[0])
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveShipping #:nodoc:
2
+ class RateEstimate
3
+ attr_reader :origin # Location objects
4
+ attr_reader :destination
5
+ attr_reader :package_rates # array of hashes in the form of {:package => <Package>, :rate => 500}
6
+ attr_reader :carrier # Carrier.name ('USPS', 'FedEx', etc.)
7
+ attr_reader :service_name # name of service ("First Class Ground", etc.)
8
+ attr_reader :service_code
9
+ attr_reader :currency # 'USD', 'CAD', etc.
10
+ # http://en.wikipedia.org/wiki/ISO_4217
11
+ attr_reader :shipping_date
12
+ attr_reader :delivery_date # Usually only available for express shipments
13
+ attr_reader :delivery_range # Min and max delivery estimate in days
14
+ attr_reader :negotiated_rate
15
+ attr_reader :insurance_price
16
+
17
+ def initialize(origin, destination, carrier, service_name, options = {})
18
+ @origin, @destination, @carrier, @service_name = origin, destination, carrier, service_name
19
+ @service_code = options[:service_code]
20
+ if options[:package_rates]
21
+ @package_rates = options[:package_rates].map { |p| p.update(:rate => Package.cents_from(p[:rate])) }
22
+ else
23
+ @package_rates = Array(options[:packages]).map { |p| {:package => p} }
24
+ end
25
+ @total_price = Package.cents_from(options[:total_price])
26
+ @negotiated_rate = options[:negotiated_rate] ? Package.cents_from(options[:negotiated_rate]) : nil
27
+ @currency = ActiveUtils::CurrencyCode.standardize(options[:currency])
28
+ @delivery_range = options[:delivery_range] ? options[:delivery_range].map { |date| date_for(date) }.compact : []
29
+ @shipping_date = date_for(options[:shipping_date])
30
+ @delivery_date = @delivery_range.last
31
+ @insurance_price = Package.cents_from(options[:insurance_price])
32
+ end
33
+
34
+ def total_price
35
+ @total_price || @package_rates.sum { |p| p[:rate] }
36
+ rescue NoMethodError
37
+ raise ArgumentError.new("RateEstimate must have a total_price set, or have a full set of valid package rates.")
38
+ end
39
+ alias_method :price, :total_price
40
+
41
+ def add(package, rate = nil)
42
+ cents = Package.cents_from(rate)
43
+ 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?
44
+ @package_rates << {:package => package, :rate => cents}
45
+ self
46
+ end
47
+
48
+ def packages
49
+ package_rates.map { |p| p[:package] }
50
+ end
51
+
52
+ def package_count
53
+ package_rates.length
54
+ end
55
+
56
+ private
57
+
58
+ def date_for(date)
59
+ date && DateTime.strptime(date.to_s, "%Y-%m-%d")
60
+ rescue ArgumentError
61
+ nil
62
+ end
63
+ end
64
+ end