active_shipping 1.11.0 → 1.11.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8af9e2153874a80f99898713503be8058c77beba
4
- data.tar.gz: 44502b5f9555c1604dc1f59d822dd9a39d63943c
3
+ metadata.gz: db9e48561ceffa9fdbe447b4f87e2c6963a390c3
4
+ data.tar.gz: b723224d9f49d015405d9617ea2c3f6f019b4157
5
5
  SHA512:
6
- metadata.gz: 234d7598e5596d9f0cea50f1923b8d678965a8b7b2b6c586c7c820948cb054480ee3a7d6e6a17fc835c6f79c180ba65338e7603d3a081b4d48d9bdefd9977397
7
- data.tar.gz: 00ee373990ad7a7f95f998bc33ace440efdad4cfe65be76e5457fc39cd2b8fce353905becef31c3e5bcce9ec5365b717f6e4c7d2a2255af91db3e1d162d6d83a
6
+ metadata.gz: 6c42dfc9fe30274791e5e2afa140af5b37f4c619e478a62cc3daff9c2c39b28d4f1d048c29fcaac26b3c34d4cbacffb663bdbdb3851b97ea335cee25701e70e8
7
+ data.tar.gz: 71a56edd9faf22dc56561b01cbb267643a2bcfc2b02eab44cd034033106bd98d17dc78f1e057736b6bef0f79a36a98f6f61845ece0036ee6a1e61d7cc687d7b8
@@ -11,63 +11,98 @@ module ActiveShipping
11
11
  # dimensions - `[5.0, 15.0, 30.0]`
12
12
  # maximum_weight - maximum weight in grams
13
13
  # currency - ISO currency code
14
- def self.pack(items, dimensions, maximum_weight, currency)
15
- return [] if items.empty?
16
- packages = []
17
-
18
- # Naive in that it assumes weight is equally distributed across all items
19
- # Should raise early enough in most cases
20
- total_weight = 0
21
- items.map!(&:symbolize_keys).each do |item|
22
- total_weight += item[:quantity].to_i * item[:grams].to_i
23
-
24
- if item[:grams].to_i > maximum_weight
25
- raise OverweightItem, "The item with weight of #{item[:grams]}g is heavier than the allowable package weight of #{maximum_weight}g"
14
+
15
+ class << self
16
+ def pack(items, dimensions, maximum_weight, currency)
17
+ return [] if items.empty?
18
+ packages = []
19
+ items.map!(&:symbolize_keys)
20
+
21
+ # Naive in that it assumes weight is equally distributed across all items
22
+ # Should raise early enough in most cases
23
+ validate_total_weight(items, maximum_weight)
24
+ items_to_pack = items.map(&:dup).sort_by! { |i| i[:grams].to_i }
25
+
26
+ state = :package_empty
27
+ while state != :packing_finished
28
+ case state
29
+ when :package_empty
30
+ package_weight, package_value = 0, 0
31
+ state = :filling_package
32
+ when :filling_package
33
+ validate_package_quantity(packages.count)
34
+
35
+ items_to_pack.each do |item|
36
+ quantity = determine_fillable_quantity_for_package(item, maximum_weight, package_weight)
37
+ package_weight += item_weight(quantity, item[:grams])
38
+ package_value += item_value(quantity, item[:price])
39
+ item[:quantity] = item[:quantity].to_i - quantity
40
+ end
41
+
42
+ items_to_pack.reject! { |i| i[:quantity].to_i == 0 }
43
+ state = :package_full
44
+ when :package_full
45
+ packages << ActiveShipping::Package.new(package_weight, dimensions, value: package_value, currency: currency)
46
+ state = items_to_pack.any? ? :package_empty : :packing_finished
47
+ end
26
48
  end
27
49
 
28
- if total_weight > maximum_weight * EXCESS_PACKAGE_QUANTITY_THRESHOLD
29
- raise ExcessPackageQuantity, "Unable to pack more than #{EXCESS_PACKAGE_QUANTITY_THRESHOLD} packages"
50
+ packages
51
+ end
52
+
53
+ private
54
+
55
+ def validate_total_weight(items, maximum_weight)
56
+ total_weight = 0
57
+ items.each do |item|
58
+ total_weight += item[:quantity].to_i * item[:grams].to_i
59
+
60
+ if overweight_item?(item[:grams], maximum_weight)
61
+ raise OverweightItem, "The item with weight of #{item[:grams]}g is heavier than the allowable package weight of #{maximum_weight}g"
62
+ end
63
+
64
+ raise_excess_quantity_error if maybe_excess_package_quantity?(total_weight, maximum_weight)
30
65
  end
31
66
  end
32
67
 
33
- items = items.map(&:dup).sort_by! { |i| i[:grams].to_i }
34
-
35
- state = :package_empty
36
- while state != :packing_finished
37
- case state
38
- when :package_empty
39
- package_weight, package_value = 0, 0
40
- state = :filling_package
41
- when :filling_package
42
- items.each do |item|
43
- quantity = if item[:grams].to_i <= 0
44
- item[:quantity].to_i
45
- else
46
- # Grab the max amount of this item we can fit into this package
47
- # Or, if there are fewer than the max for this item, put
48
- # what is left into this package
49
- [(maximum_weight - package_weight) / item[:grams].to_i, item[:quantity].to_i].min
50
- end
68
+ def validate_package_quantity(number_of_packages)
69
+ raise_excess_quantity_error if number_of_packages >= EXCESS_PACKAGE_QUANTITY_THRESHOLD
70
+ end
51
71
 
52
- item_weight = quantity * item[:grams].to_i
53
- item_value = quantity * Package.cents_from(item[:price])
72
+ def raise_excess_quantity_error
73
+ raise ExcessPackageQuantity, "Unable to pack more than #{EXCESS_PACKAGE_QUANTITY_THRESHOLD} packages"
74
+ end
54
75
 
55
- package_weight += item_weight
56
- package_value += item_value
76
+ def overweight_item?(grams, maximum_weight)
77
+ grams.to_i > maximum_weight
78
+ end
57
79
 
58
- item[:quantity] = item[:quantity].to_i - quantity
59
- end
80
+ def maybe_excess_package_quantity?(total_weight, maximum_weight)
81
+ total_weight > (maximum_weight * EXCESS_PACKAGE_QUANTITY_THRESHOLD)
82
+ end
60
83
 
61
- items.reject! { |i| i[:quantity].to_i == 0 }
84
+ def determine_fillable_quantity_for_package(item, maximum_weight, package_weight)
85
+ item_grams = item[:grams].to_i
86
+ item_quantity = item[:quantity].to_i
62
87
 
63
- state = :package_full
64
- when :package_full
65
- packages << ActiveShipping::Package.new(package_weight, dimensions, :value => package_value, :currency => currency)
66
- state = items.any? ? :package_empty : :packing_finished
88
+ if item_grams <= 0
89
+ item_quantity
90
+ else
91
+ # Grab the max amount of this item we can fit into this package
92
+ # Or, if there are fewer than the max for this item, put
93
+ # what is left into this package
94
+ available_grams = (maximum_weight - package_weight).to_i
95
+ [available_grams / item_grams, item_quantity].min
67
96
  end
68
97
  end
69
98
 
70
- packages
99
+ def item_weight(quantity, grams)
100
+ quantity * grams.to_i
101
+ end
102
+
103
+ def item_value(quantity, price)
104
+ quantity * Package.cents_from(price)
105
+ end
71
106
  end
72
107
  end
73
108
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveShipping
2
- VERSION = "1.11.0"
2
+ VERSION = "1.11.1"
3
3
  end
@@ -6,7 +6,7 @@ class ShipmentPackerTest < Minitest::Test
6
6
  end
7
7
 
8
8
  def test_pack_divide_order_into_a_single_package
9
- items = [{:grams => 1, :quantity => 1, :price => 1.0}]
9
+ items = [{ grams: 1, quantity: 1, price: 1.0 }]
10
10
 
11
11
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
12
12
  assert_equal 1, packages.size
@@ -16,7 +16,7 @@ class ShipmentPackerTest < Minitest::Test
16
16
  end
17
17
 
18
18
  def test_divide_order_with_multiple_lines_into_a_single_package
19
- items = [{:grams => 1, :quantity => 2, :price => 1.0}]
19
+ items = [{ grams: 1, quantity: 2, price: 1.0 }]
20
20
 
21
21
  packages = ShipmentPacker.pack(items, @dimensions, 2, 'USD')
22
22
  assert_equal 1, packages.size
@@ -26,7 +26,7 @@ class ShipmentPackerTest < Minitest::Test
26
26
  end
27
27
 
28
28
  def test_divide_order_with_single_line_into_two_packages
29
- items = [{:grams => 1, :quantity => 2, :price => 1.0}]
29
+ items = [{ grams: 1, quantity: 2, price: 1.0 }]
30
30
 
31
31
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
32
32
  assert_equal 2, packages.size
@@ -36,10 +36,23 @@ class ShipmentPackerTest < Minitest::Test
36
36
  end
37
37
  end
38
38
 
39
+ def test_divide_order_with_single_line_into_two_packages_max_weight_as_float
40
+ max_weight = 68038.8555
41
+
42
+ items = [{ grams: 45359, quantity: 2, price: 1.0 }]
43
+
44
+ packages = ShipmentPacker.pack(items, @dimensions, max_weight, 'USD')
45
+ assert_equal 2, packages.size
46
+
47
+ packages.each do |package|
48
+ assert_equal 45359, package.weight
49
+ end
50
+ end
51
+
39
52
  def test_divide_order_with_multiple_lines_into_two_packages
40
53
  items = [
41
- {:grams => 1, :quantity => 1, :price => 1.0},
42
- {:grams => 1, :quantity => 1, :price => 1.0}
54
+ { grams: 1, quantity: 1, price: 1.0 },
55
+ { grams: 1, quantity: 1, price: 1.0 }
43
56
  ]
44
57
 
45
58
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
@@ -52,9 +65,9 @@ class ShipmentPackerTest < Minitest::Test
52
65
 
53
66
  def test_divide_order_into_two_packages_mixing_line_items
54
67
  items = [
55
- {:grams => 1, :quantity => 1, :price => 1.0},
56
- {:grams => 1, :quantity => 1, :price => 1.0},
57
- {:grams => 1, :quantity => 1, :price => 1.0}
68
+ { grams: 1, quantity: 1, price: 1.0 },
69
+ { grams: 1, quantity: 1, price: 1.0 },
70
+ { grams: 1, quantity: 1, price: 1.0 }
58
71
  ]
59
72
 
60
73
  packages = ShipmentPacker.pack(items, @dimensions, 2, 'USD')
@@ -66,16 +79,16 @@ class ShipmentPackerTest < Minitest::Test
66
79
 
67
80
  def test_raise_overweight_exception_when_a_single_item_exceeds_the_maximum_weight_of_a_package
68
81
  assert_raises(ShipmentPacker::OverweightItem) do
69
- items = [{:grams => 2, :quantity => 1, :price => 1.0}]
82
+ items = [{ grams: 2, quantity: 1, price: 1.0 }]
70
83
  ShipmentPacker.pack(items, @dimensions, 1, 'USD')
71
84
  end
72
85
  end
73
86
 
74
87
  def test_raise_over_weight_exceptions_before_over_package_limit_exceptions
75
- assert_raises(ShipmentPacker::OverweightItem) do
76
- items = [{:grams => 5, :quantity => ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD + 1, :price => 1.0}]
77
- ShipmentPacker.pack(items, @dimensions, 4, 'USD')
78
- end
88
+ assert_raises(ShipmentPacker::OverweightItem) do
89
+ items = [{ grams: 5, quantity: ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD + 1, price: 1.0 }]
90
+ ShipmentPacker.pack(items, @dimensions, 4, 'USD')
91
+ end
79
92
  end
80
93
 
81
94
  def test_returns_an_empty_list_when_no_items_provided
@@ -84,8 +97,8 @@ class ShipmentPackerTest < Minitest::Test
84
97
 
85
98
  def test_add_summarized_prices_for_all_items_and_currency_to_package
86
99
  items = [
87
- {:grams => 1, :quantity => 3, :price => 1.0},
88
- {:grams => 2, :quantity => 1, :price => 2.0}
100
+ { grams: 1, quantity: 3, price: 1.0 },
101
+ { grams: 2, quantity: 1, price: 2.0 }
89
102
  ]
90
103
  packages = ShipmentPacker.pack(items, @dimensions, 5, 'USD')
91
104
  assert_equal 1, packages.size
@@ -95,9 +108,9 @@ class ShipmentPackerTest < Minitest::Test
95
108
 
96
109
  def test_divide_items_and_prices_accordingly_when_splitting_into_two_packages
97
110
  items = [
98
- {:grams => 1, :quantity => 1, :price => 1.0},
99
- {:grams => 1, :quantity => 1, :price => 1.0},
100
- {:grams => 1, :quantity => 1, :price => 1.0}
111
+ { grams: 1, quantity: 1, price: 1.0 },
112
+ { grams: 1, quantity: 1, price: 1.0 },
113
+ { grams: 1, quantity: 1, price: 1.0 }
101
114
  ]
102
115
 
103
116
  packages = ShipmentPacker.pack(items, @dimensions, 2, 'USD')
@@ -110,8 +123,8 @@ class ShipmentPackerTest < Minitest::Test
110
123
  end
111
124
 
112
125
  def test_symbolize_item_keys
113
- string_key_items = [{'grams' => 1, 'quantity' => 1, 'price' => 1.0}]
114
- indifferent_access_items = [{'grams' => 1, 'quantity' => 1, 'price' => 1.0}.with_indifferent_access]
126
+ string_key_items = [{ 'grams' => 1, 'quantity' => 1, 'price' => 1.0 }]
127
+ indifferent_access_items = [{ 'grams' => 1, 'quantity' => 1, 'price' => 1.0 }.with_indifferent_access]
115
128
 
116
129
  [string_key_items, indifferent_access_items].each do |items|
117
130
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
@@ -124,7 +137,7 @@ class ShipmentPackerTest < Minitest::Test
124
137
  end
125
138
 
126
139
  def test_cast_quantity_and_grams_to_int
127
- items = [{:grams => '1', :quantity => '1', :price => '1.0'}]
140
+ items = [{ grams: '1', quantity: '1', price: '1.0' }]
128
141
 
129
142
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
130
143
 
@@ -133,15 +146,40 @@ class ShipmentPackerTest < Minitest::Test
133
146
  assert_equal 100, package.value
134
147
  end
135
148
 
136
- def test_excess_packages
149
+ def test_excess_packages_raised_over_threshold_before_packing_begins
150
+ ActiveShipping::Package.expects(:new).never
151
+ items = [{ grams: 1, quantity: ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD + 1, price: 1.0 }]
152
+
137
153
  assert_raises(ShipmentPacker::ExcessPackageQuantity) do
138
- items = [{:grams => 1, :quantity => ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD + 1, :price => 1.0}]
139
154
  ShipmentPacker.pack(items, @dimensions, 1, 'USD')
140
155
  end
141
156
  end
142
157
 
158
+ def test_excess_packages_not_raised_at_threshold
159
+ items = [{ grams: 1, quantity: ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD, price: 1.0 }]
160
+ packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
161
+
162
+ assert_predicate packages, :present?
163
+ end
164
+
165
+ def test_excess_packages_not_raised_below_threshold
166
+ items = [{ grams: 1, quantity: ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD - 1, price: 1.0 }]
167
+ packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
168
+
169
+ assert_predicate packages, :present?
170
+ end
171
+
172
+ def test_excess_packages_with_slightly_larger_max_weight_than_item_weight
173
+ max_weight = 750
174
+ items = [{ grams: 500, quantity: ShipmentPacker::EXCESS_PACKAGE_QUANTITY_THRESHOLD + 1, price: 1.0 }]
175
+
176
+ assert_raises(ShipmentPacker::ExcessPackageQuantity) do
177
+ ShipmentPacker.pack(items, @dimensions, max_weight, 'USD')
178
+ end
179
+ end
180
+
143
181
  def test_lots_of_zero_weight_items
144
- items = [{:grams => 0, :quantity => 1_000_000, :price => 1.0}]
182
+ items = [{ grams: 0, quantity: 1_000_000, price: 1.0 }]
145
183
  packages = ShipmentPacker.pack(items, @dimensions, 1, 'USD')
146
184
 
147
185
  assert_equal 1, packages.size
@@ -150,7 +188,7 @@ class ShipmentPackerTest < Minitest::Test
150
188
  end
151
189
 
152
190
  def test_dont_destroy_input_items
153
- items = [{:grams => 1, :quantity => 5, :price => 1.0}]
191
+ items = [{ grams: 1, quantity: 5, price: 1.0 }]
154
192
 
155
193
  packages = ShipmentPacker.pack(items, @dimensions, 10, 'USD')
156
194
 
@@ -159,14 +197,14 @@ class ShipmentPackerTest < Minitest::Test
159
197
  end
160
198
 
161
199
  def test_dont_modify_input_item_quantities
162
- items = [{:grams => 1, :quantity => 5, :price => 1.0}]
200
+ items = [{ grams: 1, quantity: 5, price: 1.0 }]
163
201
 
164
202
  ShipmentPacker.pack(items, @dimensions, 10, 'USD')
165
203
  assert_equal 5, items.first[:quantity]
166
204
  end
167
205
 
168
206
  def test_items_with_negative_weight
169
- items = [{:grams => -1, :quantity => 5, :price => 1.0}]
207
+ items = [{ grams: -1, quantity: 5, price: 1.0 }]
170
208
 
171
209
  ShipmentPacker.pack(items, @dimensions, 10, 'USD')
172
210
  assert_equal 5, items.first[:quantity]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_shipping
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.0
4
+ version: 1.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-13 00:00:00.000000000 Z
11
+ date: 2017-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: quantified