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 +4 -4
- data/lib/active_shipping/shipment_packer.rb +79 -44
- data/lib/active_shipping/version.rb +1 -1
- data/test/unit/shipment_packer_test.rb +65 -27
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db9e48561ceffa9fdbe447b4f87e2c6963a390c3
|
|
4
|
+
data.tar.gz: b723224d9f49d015405d9617ea2c3f6f019b4157
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
-
|
|
72
|
+
def raise_excess_quantity_error
|
|
73
|
+
raise ExcessPackageQuantity, "Unable to pack more than #{EXCESS_PACKAGE_QUANTITY_THRESHOLD} packages"
|
|
74
|
+
end
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
def overweight_item?(grams, maximum_weight)
|
|
77
|
+
grams.to_i > maximum_weight
|
|
78
|
+
end
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
def maybe_excess_package_quantity?(total_weight, maximum_weight)
|
|
81
|
+
total_weight > (maximum_weight * EXCESS_PACKAGE_QUANTITY_THRESHOLD)
|
|
82
|
+
end
|
|
60
83
|
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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 = [{:
|
|
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 = [{:
|
|
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 = [{:
|
|
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
|
-
{:
|
|
42
|
-
{:
|
|
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
|
-
{:
|
|
56
|
-
{:
|
|
57
|
-
{:
|
|
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 = [{:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
{:
|
|
88
|
-
{:
|
|
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
|
-
{:
|
|
99
|
-
{:
|
|
100
|
-
{:
|
|
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 = [{:
|
|
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
|
|
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 = [{:
|
|
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 = [{:
|
|
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 = [{:
|
|
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 = [{:
|
|
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.
|
|
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-
|
|
11
|
+
date: 2017-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: quantified
|