active_shipping 1.11.0 → 1.11.1

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.
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