spree_boxnow 1.0.0 → 1.2.0
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/README.md +33 -32
- data/app/lib/spree_boxnow/phone_normalizer.rb +19 -0
- data/app/models/spree/calculator/shipping/boxnow_rate.rb +152 -37
- data/app/services/spree_boxnow/create_voucher.rb +2 -2
- data/lib/spree_boxnow/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f62d5fc8bf2894744140e90eba7aa7c6eeeda3b45f5d2002631196747ab663e7
|
|
4
|
+
data.tar.gz: 87466f19dc78f545ec26bc3e140f53c9759fecb9a57f811853362b91b4000b69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d1b9a5368eef166ef932bb2b53e93930f57ddc911618efbd97acaec6154004fa7f870dedb86fa1a05b0fcb3e97521ab3858fd5fbcdb03ef1002d7d73d531e69
|
|
7
|
+
data.tar.gz: e641fb1f659b9c27d2275127f17144472e109c42762291aad553f49d34d0e04d9eda07be8cade5d1e126f066c4d161e1c1d8b372a6d16ef10b2d82b73f296f08
|
data/README.md
CHANGED
|
@@ -34,16 +34,16 @@ A [Spree Commerce](https://spreecommerce.org) extension that integrates **BoxNow
|
|
|
34
34
|
|
|
35
35
|
Go to **Admin → Integrations → BoxNow** and fill in:
|
|
36
36
|
|
|
37
|
-
| Field
|
|
38
|
-
|
|
39
|
-
| **Client ID**
|
|
40
|
-
| **Client Secret**
|
|
41
|
-
| **Partner ID**
|
|
42
|
-
| **API URL**
|
|
43
|
-
| **Origin Location ID** | Your warehouse/store APM location ID
|
|
44
|
-
| **Contact Name**
|
|
45
|
-
| **Contact Phone**
|
|
46
|
-
| **Contact Email**
|
|
37
|
+
| Field | Description |
|
|
38
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
39
|
+
| **Client ID** | Provided by BoxNow |
|
|
40
|
+
| **Client Secret** | Provided by BoxNow |
|
|
41
|
+
| **Partner ID** | Provided by BoxNow |
|
|
42
|
+
| **API URL** | BoxNow API base URL (e.g. `https://api-production.boxnow.gr`) |
|
|
43
|
+
| **Origin Location ID** | Your warehouse/store APM location ID |
|
|
44
|
+
| **Contact Name** | Sender contact name printed on labels |
|
|
45
|
+
| **Contact Phone** | Sender phone number |
|
|
46
|
+
| **Contact Email** | Sender email address |
|
|
47
47
|
|
|
48
48
|
OAuth2 tokens are obtained automatically using Client Credentials and cached in `Rails.cache` for 1 hour.
|
|
49
49
|
|
|
@@ -56,12 +56,12 @@ The shipping calculator determines the price tier from the physical dimensions o
|
|
|
56
56
|
|
|
57
57
|
BoxNow hard limits:
|
|
58
58
|
|
|
59
|
-
| Dimension
|
|
60
|
-
|
|
59
|
+
| Dimension | Limit |
|
|
60
|
+
| ---------- | ----- |
|
|
61
61
|
| Max weight | 20 kg |
|
|
62
62
|
| Max height | 36 cm |
|
|
63
|
-
| Max width
|
|
64
|
-
| Max depth
|
|
63
|
+
| Max width | 45 cm |
|
|
64
|
+
| Max depth | 60 cm |
|
|
65
65
|
|
|
66
66
|
### 3. Create a BoxNow shipping method
|
|
67
67
|
|
|
@@ -72,13 +72,13 @@ Go to **Admin → Shipping Methods → New**:
|
|
|
72
72
|
3. Select **BoxNow Rate** as the calculator
|
|
73
73
|
4. Set the calculator preferences:
|
|
74
74
|
|
|
75
|
-
| Preference
|
|
76
|
-
|
|
77
|
-
| **Small box price**
|
|
78
|
-
| **Medium box price**
|
|
79
|
-
| **Large box price**
|
|
80
|
-
| **Base padding (cm)** | 1.0
|
|
81
|
-
| **Multi-item factor** | 1.05
|
|
75
|
+
| Preference | Default | Description |
|
|
76
|
+
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| **Small box price** | 0.0 | Price for parcels ≤ 8 cm in height |
|
|
78
|
+
| **Medium box price** | 0.0 | Price for parcels ≤ 17 cm in height |
|
|
79
|
+
| **Large box price** | 0.0 | Price for parcels ≤ 36 cm in height |
|
|
80
|
+
| **Base padding (cm)** | 1.0 | Added to every dimension to account for the physical box being slightly larger than its contents. Set to `0` to disable. |
|
|
81
|
+
| **Multi-item factor** | 1.05 | Multiplier applied to all dimensions when an order has more than one item (accounts for imperfect stacking). Set to `1.0` to disable. |
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -87,17 +87,18 @@ Go to **Admin → Shipping Methods → New**:
|
|
|
87
87
|
The calculator models the entire order as **one parcel** — there is no multi-box splitting.
|
|
88
88
|
|
|
89
89
|
For each line item it:
|
|
90
|
+
|
|
90
91
|
1. Sorts the variant's three dimensions smallest → largest (`s ≤ m ≤ d`)
|
|
91
92
|
2. Stacks items along the smallest axis: `parcel_height += s × quantity`
|
|
92
93
|
3. Takes `width = max(m)` and `depth = max(d)` across all items
|
|
93
94
|
|
|
94
95
|
After stacking, padding and the multi-item factor are applied. The resulting three dimensions are then sorted again to allow virtual rotation, and the smallest is compared against the tier thresholds.
|
|
95
96
|
|
|
96
|
-
| Tier
|
|
97
|
-
|
|
98
|
-
| Small
|
|
99
|
-
| Medium | ≤ 17 cm
|
|
100
|
-
| Large
|
|
97
|
+
| Tier | Height threshold |
|
|
98
|
+
| ------ | ---------------- |
|
|
99
|
+
| Small | ≤ 8 cm |
|
|
100
|
+
| Medium | ≤ 17 cm |
|
|
101
|
+
| Large | ≤ 36 cm |
|
|
101
102
|
|
|
102
103
|
If the package exceeds the large threshold or any hard limit, `nil` is returned and the BoxNow shipping option is hidden from checkout.
|
|
103
104
|
|
|
@@ -159,12 +160,12 @@ If a voucher creation attempt fails and the admin retries, the extension appends
|
|
|
159
160
|
|
|
160
161
|
## Routes
|
|
161
162
|
|
|
162
|
-
| Method | Path
|
|
163
|
-
|
|
164
|
-
| `POST` | `/boxnow/select_locker`
|
|
165
|
-
| `POST` | `/{admin_path}/boxnow/:order_id/create`
|
|
166
|
-
| `GET`
|
|
167
|
-
| `POST` | `/{admin_path}/boxnow/:order_id/cancel`
|
|
163
|
+
| Method | Path | Action |
|
|
164
|
+
| ------ | ---------------------------------------------- | ------------------------------------------------- |
|
|
165
|
+
| `POST` | `/boxnow/select_locker` | Storefront: save locker selection |
|
|
166
|
+
| `POST` | `/{admin_path}/boxnow/:order_id/create` | Admin: create voucher |
|
|
167
|
+
| `GET` | `/{admin_path}/boxnow/:order_id/print` | Admin: print/download voucher PDF |
|
|
168
|
+
| `POST` | `/{admin_path}/boxnow/:order_id/cancel` | Admin: cancel voucher |
|
|
168
169
|
| `POST` | `/{admin_path}/boxnow/:order_id/select_locker` | Admin: update locker selection (pre-voucher only) |
|
|
169
170
|
|
|
170
171
|
---
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'phonelib'
|
|
2
|
+
|
|
3
|
+
module SpreeBoxnow
|
|
4
|
+
# Normalizes phone numbers to the full international (E.164) format BoxNow
|
|
5
|
+
# requires, e.g. +306912345678. `country_iso` is the ISO 3166-1 alpha-2
|
|
6
|
+
# code used to interpret a number given in national format (defaults to
|
|
7
|
+
# GR, since BoxNow only operates in Greece); numbers already given in
|
|
8
|
+
# international format (leading +) are parsed as-is regardless.
|
|
9
|
+
module PhoneNormalizer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(phone, country_iso = nil)
|
|
13
|
+
return '' if phone.blank?
|
|
14
|
+
|
|
15
|
+
parsed = Phonelib.parse(phone, country_iso || 'GR')
|
|
16
|
+
parsed.valid? ? parsed.e164 : phone.to_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -7,7 +7,7 @@ module Spree
|
|
|
7
7
|
MAX_WIDTH_CM = 45.0
|
|
8
8
|
MAX_DEPTH_CM = 60.0
|
|
9
9
|
|
|
10
|
-
# Size thresholds
|
|
10
|
+
# Size thresholds — tier is determined by the smallest sorted dimension
|
|
11
11
|
SIZE_MAX_HEIGHT_CM = {
|
|
12
12
|
small: 8.0,
|
|
13
13
|
medium: 17.0,
|
|
@@ -18,8 +18,8 @@ module Spree
|
|
|
18
18
|
preference :medium_box_price, :decimal, default: 0.0
|
|
19
19
|
preference :large_box_price, :decimal, default: 0.0
|
|
20
20
|
|
|
21
|
-
preference :base_padding_cm,
|
|
22
|
-
preference :multi_item_factor,
|
|
21
|
+
preference :base_padding_cm, :decimal, default: 1.0
|
|
22
|
+
preference :multi_item_factor, :decimal, default: 1.05
|
|
23
23
|
|
|
24
24
|
validates :preferred_small_box_price,
|
|
25
25
|
:preferred_medium_box_price,
|
|
@@ -32,53 +32,163 @@ module Spree
|
|
|
32
32
|
Spree.t(:shipping_boxnow_rate)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
SIZES_ORDERED = %i[small medium large].freeze
|
|
36
|
+
|
|
35
37
|
def compute_package(package)
|
|
36
38
|
return nil if package.weight.to_f > MAX_WEIGHT_KG
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
return nil if
|
|
40
|
+
boxes = minimum_bounding_boxes(package)
|
|
41
|
+
return nil if boxes.nil?
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
best_size = nil
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
boxes.each do |sml|
|
|
46
|
+
padded = apply_packing_margin({ height: sml[0], width: sml[1], depth: sml[2] }, package)
|
|
47
|
+
s, m, d = [padded[:height], padded[:width], padded[:depth]].map(&:to_f).sort
|
|
48
|
+
next if s > MAX_HEIGHT_CM || m > MAX_WIDTH_CM || d > MAX_DEPTH_CM
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
size = box_size_for_height(s)
|
|
51
|
+
next unless size
|
|
52
|
+
|
|
53
|
+
best_size = smaller_size(best_size, size)
|
|
54
|
+
end
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
return nil unless best_size
|
|
57
|
+
|
|
58
|
+
price_for(best_size)
|
|
51
59
|
end
|
|
52
60
|
|
|
53
61
|
private
|
|
54
62
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
depths << d
|
|
63
|
+
# Exhaustive recursive bounding-box search over all item orientations and
|
|
64
|
+
# all possible groupings (hierarchical binary splits).
|
|
65
|
+
#
|
|
66
|
+
# Each line item (distinct SKU) is treated as a block in one of up to 3
|
|
67
|
+
# orientations — one per choice of which dimension is multiplied by qty.
|
|
68
|
+
# The solver tries every binary partition of line items into two groups,
|
|
69
|
+
# combines the Pareto-optimal bounding boxes of each group along 3 axes,
|
|
70
|
+
# and Pareto-prunes the result. Memoisation by sorted index set ensures
|
|
71
|
+
# each subset is computed exactly once.
|
|
72
|
+
#
|
|
73
|
+
# N = distinct line items (SKUs), not total quantity.
|
|
74
|
+
# 100 identical keycards = 1 line item = N=1 → trivially fast.
|
|
75
|
+
# For very large N (many distinct SKUs), a time-based execution guard
|
|
76
|
+
# can be added if observed to be slow in production.
|
|
77
|
+
def minimum_bounding_boxes(package)
|
|
78
|
+
all_items = package.contents.map do |content|
|
|
79
|
+
orientations = item_block_orientations(content.variant, content.quantity)
|
|
80
|
+
return nil if orientations.nil?
|
|
81
|
+
orientations
|
|
75
82
|
end
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
return all_items[0] if all_items.size == 1
|
|
85
|
+
|
|
86
|
+
solve_boxes((0...all_items.size).to_a, all_items, {})
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Recursive memoised solver.
|
|
90
|
+
# indices — sorted Array<Integer> identifying this subset within all_items
|
|
91
|
+
# all_items — Array<Array<[s,m,l]>>, per-item orientation sets
|
|
92
|
+
# cache — Hash keyed by sorted index array
|
|
93
|
+
def solve_boxes(indices, all_items, cache)
|
|
94
|
+
return cache[indices] if cache.key?(indices)
|
|
95
|
+
|
|
96
|
+
result =
|
|
97
|
+
if indices.size == 1
|
|
98
|
+
all_items[indices[0]]
|
|
99
|
+
else
|
|
100
|
+
# Pin indices[0] in the left group and vary what else joins it.
|
|
101
|
+
# This enumerates every unordered binary partition exactly once.
|
|
102
|
+
rest = indices[1..]
|
|
103
|
+
combined = []
|
|
104
|
+
|
|
105
|
+
0.upto(rest.size - 1) do |left_size|
|
|
106
|
+
rest.combination(left_size).each do |left_extra|
|
|
107
|
+
left_indices = ([indices[0]] + left_extra).sort
|
|
108
|
+
right_indices = (rest - left_extra).sort
|
|
109
|
+
next if right_indices.empty?
|
|
110
|
+
|
|
111
|
+
boxes_a = solve_boxes(left_indices, all_items, cache)
|
|
112
|
+
boxes_b = solve_boxes(right_indices, all_items, cache)
|
|
113
|
+
|
|
114
|
+
boxes_a.each do |a|
|
|
115
|
+
boxes_b.each do |b|
|
|
116
|
+
combined.concat(combine_sorted_boxes(a, b))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
pareto_optimal(combined)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
cache[indices] = result
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Combines two sorted triples [s1,m1,l1] and [s2,m2,l2] along each of
|
|
129
|
+
# the 3 axes (sum one dimension, max the other two), re-sorts each result
|
|
130
|
+
# to model free physical rotation, and returns the Pareto-optimal subset.
|
|
131
|
+
def combine_sorted_boxes(box_a, box_b)
|
|
132
|
+
s1, m1, l1 = box_a
|
|
133
|
+
s2, m2, l2 = box_b
|
|
134
|
+
|
|
135
|
+
pareto_optimal([
|
|
136
|
+
[s1 + s2, [m1, m2].max, [l1, l2].max].sort,
|
|
137
|
+
[[s1, s2].max, m1 + m2, [l1, l2].max].sort,
|
|
138
|
+
[[s1, s2].max, [m1, m2].max, l1 + l2 ].sort
|
|
139
|
+
])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Removes dominated boxes from a collection of sorted triples.
|
|
143
|
+
# Box X dominates box Y if X[i] <= Y[i] for all i with at least one strict.
|
|
144
|
+
def pareto_optimal(boxes)
|
|
145
|
+
boxes.reject do |candidate|
|
|
146
|
+
boxes.any? do |other|
|
|
147
|
+
next false if other.equal?(candidate)
|
|
148
|
+
other[0] <= candidate[0] &&
|
|
149
|
+
other[1] <= candidate[1] &&
|
|
150
|
+
other[2] <= candidate[2] &&
|
|
151
|
+
other != candidate
|
|
152
|
+
end
|
|
153
|
+
end.uniq
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns all distinct sorted [s,m,l] orientations for a line item.
|
|
157
|
+
# For each factorization of qty into (a,b,c) with a*b*c == qty, and for
|
|
158
|
+
# each permutation of (a,b,c) assigned to the item's 3 dimensions, the
|
|
159
|
+
# bounding box is computed and sorted (free rotation). This covers linear
|
|
160
|
+
# stacking, 2-D grids, and 3-D grids (e.g. 2x5 arrangement of 10 items).
|
|
161
|
+
def item_block_orientations(variant, qty)
|
|
162
|
+
dims = variant_dimensions_cm(variant)
|
|
163
|
+
return nil if dims.nil?
|
|
164
|
+
|
|
165
|
+
h, w, d = dims
|
|
166
|
+
orientations = []
|
|
167
|
+
|
|
168
|
+
qty_factorizations(qty).each do |a, b, c|
|
|
169
|
+
[a, b, c].permutation.each do |fa, fb, fc|
|
|
170
|
+
orientations << [h * fa, w * fb, d * fc].sort
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
orientations.uniq
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns all sorted triples [a, b, c] with a <= b <= c and a*b*c == n.
|
|
178
|
+
def qty_factorizations(n)
|
|
179
|
+
result = []
|
|
180
|
+
a = 1
|
|
181
|
+
while a * a * a <= n
|
|
182
|
+
if n % a == 0
|
|
183
|
+
b = a
|
|
184
|
+
while a * b * b <= n
|
|
185
|
+
result << [a, b, n / (a * b)] if (n / a) % b == 0
|
|
186
|
+
b += 1
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
a += 1
|
|
190
|
+
end
|
|
191
|
+
result
|
|
82
192
|
end
|
|
83
193
|
|
|
84
194
|
def apply_packing_margin(dims, package)
|
|
@@ -105,6 +215,11 @@ module Spree
|
|
|
105
215
|
[height, width, depth]
|
|
106
216
|
end
|
|
107
217
|
|
|
218
|
+
def smaller_size(a, b)
|
|
219
|
+
return b if a.nil?
|
|
220
|
+
SIZES_ORDERED.index(a) <= SIZES_ORDERED.index(b) ? a : b
|
|
221
|
+
end
|
|
222
|
+
|
|
108
223
|
def box_size_for_height(height_cm)
|
|
109
224
|
return :small if height_cm <= SIZE_MAX_HEIGHT_CM[:small]
|
|
110
225
|
return :medium if height_cm <= SIZE_MAX_HEIGHT_CM[:medium]
|
|
@@ -31,13 +31,13 @@ module SpreeBoxnow
|
|
|
31
31
|
origin: {
|
|
32
32
|
locationId: integration.preferred_origin_location_id,
|
|
33
33
|
contactName: integration.preferred_contact_name.presence || '',
|
|
34
|
-
contactNumber: integration.preferred_contact_phone
|
|
34
|
+
contactNumber: PhoneNormalizer.call(integration.preferred_contact_phone),
|
|
35
35
|
contactEmail: integration.preferred_contact_email.presence || ''
|
|
36
36
|
},
|
|
37
37
|
destination: {
|
|
38
38
|
locationId: destination_id,
|
|
39
39
|
contactName: address.full_name,
|
|
40
|
-
contactNumber: address.phone.
|
|
40
|
+
contactNumber: PhoneNormalizer.call(address.phone, address.country_iso),
|
|
41
41
|
contactEmail: order.email.to_s
|
|
42
42
|
},
|
|
43
43
|
items: [{
|
data/lib/spree_boxnow/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_boxnow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OlympusOne
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '2.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: phonelib
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.10'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.10'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: spree_dev_tools
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -136,6 +150,7 @@ files:
|
|
|
136
150
|
- app/javascript/spree_boxnow/application.js
|
|
137
151
|
- app/javascript/spree_boxnow/controllers/spree_boxnow_controller.js
|
|
138
152
|
- app/lib/spree_boxnow/api_client.rb
|
|
153
|
+
- app/lib/spree_boxnow/phone_normalizer.rb
|
|
139
154
|
- app/models/spree/calculator/shipping/boxnow_rate.rb
|
|
140
155
|
- app/models/spree/integrations/boxnow.rb
|
|
141
156
|
- app/models/spree/order_decorator.rb
|
|
@@ -167,10 +182,10 @@ licenses:
|
|
|
167
182
|
- MIT
|
|
168
183
|
metadata:
|
|
169
184
|
bug_tracker_uri: https://github.com/olympusone/spree_boxnow/issues
|
|
170
|
-
changelog_uri: https://github.com/olympusone/spree_boxnow/releases/tag/v1.
|
|
185
|
+
changelog_uri: https://github.com/olympusone/spree_boxnow/releases/tag/v1.2.0
|
|
171
186
|
documentation_uri: https://github.com/olympusone/spree_boxnow
|
|
172
187
|
homepage_uri: https://github.com/olympusone/spree_boxnow
|
|
173
|
-
source_code_uri: https://github.com/olympusone/spree_boxnow/tree/v1.
|
|
188
|
+
source_code_uri: https://github.com/olympusone/spree_boxnow/tree/v1.2.0
|
|
174
189
|
rdoc_options: []
|
|
175
190
|
require_paths:
|
|
176
191
|
- lib
|