tracking_number 0.10.5 → 3.0.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 +5 -5
- data/.github/FUNDING.yml +2 -0
- data/.github/dependabot.yml +22 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitmodules +3 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +318 -0
- data/Gemfile +4 -2
- data/README.md +112 -15
- data/Rakefile +4 -1
- data/bin/console +14 -0
- data/lib/data/couriers/amazon.json +45 -0
- data/lib/data/couriers/canadapost.json +33 -0
- data/lib/data/couriers/canpar.json +29 -0
- data/lib/data/couriers/dhl.json +97 -0
- data/lib/data/couriers/dpd.json +345 -0
- data/lib/data/couriers/fedex.json +291 -0
- data/lib/data/couriers/gofo.json +24 -0
- data/lib/data/couriers/landmark.json +26 -0
- data/lib/data/couriers/lasership.json +105 -0
- data/lib/data/couriers/old_dominion.json +65 -0
- data/lib/data/couriers/ontrac.json +74 -0
- data/lib/data/couriers/purolator.json +50 -0
- data/lib/data/couriers/s10.json +1443 -0
- data/lib/data/couriers/speedee.json +26 -0
- data/lib/data/couriers/ups.json +197 -0
- data/lib/data/couriers/usps.json +272 -0
- data/lib/data/couriers/yodel.json +29 -0
- data/lib/data/couriers/yunexpress.json +25 -0
- data/lib/tracking_number/active_model_validator.rb +1 -1
- data/lib/tracking_number/base.rb +291 -22
- data/lib/tracking_number/checksum_validations.rb +119 -0
- data/lib/tracking_number/info.rb +26 -0
- data/lib/tracking_number/loader.rb +82 -0
- data/lib/tracking_number/partnership.rb +7 -0
- data/lib/tracking_number/unknown.rb +45 -0
- data/lib/tracking_number/version.rb +1 -1
- data/lib/tracking_number.rb +69 -17
- data/package.json +77 -0
- data/test/partnership_test.rb +71 -0
- data/test/renamed_types_test.rb +44 -0
- data/test/test_helper.rb +14 -7
- data/test/tracking_number_meta_test.rb +145 -0
- data/test/tracking_number_test.rb +191 -0
- data/tracking_number.gemspec +53 -22
- data/yarn.lock +3425 -0
- metadata +131 -46
- data/.travis.yml +0 -12
- data/VERSION +0 -1
- data/lib/tracking_number/dhl.rb +0 -34
- data/lib/tracking_number/fedex.rb +0 -156
- data/lib/tracking_number/ontrac.rb +0 -34
- data/lib/tracking_number/ups.rb +0 -108
- data/lib/tracking_number/usps.rb +0 -166
- data/test/dhl_tracking_number_test.rb +0 -35
- data/test/fedex_tracking_number_test.rb +0 -75
- data/test/ontrac_tracking_number_test.rb +0 -24
- data/test/ups_tracking_number_test.rb +0 -36
- data/test/usps_tracking_number_test.rb +0 -73
data/lib/tracking_number/base.rb
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
module TrackingNumber
|
|
2
2
|
class Base
|
|
3
|
-
attr_accessor :tracking_number
|
|
4
|
-
|
|
3
|
+
attr_accessor :tracking_number, :original_number, :partner, :partner_data
|
|
4
|
+
|
|
5
|
+
PartnerStruct = Struct.new(:shipper, :carrier)
|
|
5
6
|
|
|
6
7
|
def initialize(tracking_number)
|
|
7
8
|
@original_number = tracking_number
|
|
8
|
-
@tracking_number = tracking_number.strip.gsub(
|
|
9
|
+
@tracking_number = tracking_number.strip.gsub(' ', '').upcase
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def self.search(body)
|
|
12
|
-
valids =
|
|
13
|
+
valids = scan(body).uniq.collect { |possible| new(possible) }.select { |t| t.valid? }
|
|
13
14
|
|
|
14
15
|
uniques = {}
|
|
15
16
|
valids.each do |t|
|
|
@@ -20,48 +21,316 @@ module TrackingNumber
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def self.scan(body)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# matches with match groups within the match data
|
|
25
|
+
matches = []
|
|
26
|
+
|
|
27
|
+
body.upcase.scan(const_get(:SEARCH_PATTERN)) do
|
|
28
|
+
# get the match data instead, which is needed with these types of regexes
|
|
29
|
+
matches << $~
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if matches
|
|
33
|
+
matches.collect { |m| m[0] }
|
|
34
|
+
else
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def serial_number
|
|
40
|
+
return match_group('SerialNumber') unless self.class.const_get('VALIDATION')
|
|
41
|
+
|
|
42
|
+
format_info = self.class.const_get(:VALIDATION)[:serial_number_format]
|
|
43
|
+
raw_serial = match_group('SerialNumber')
|
|
44
|
+
|
|
45
|
+
if format_info && format_info[:prepend_if] && raw_serial.match(Regexp.new(format_info[:prepend_if][:matches_regex]))
|
|
46
|
+
return "#{format_info[:prepend_if][:content]}#{raw_serial}"
|
|
47
|
+
# elsif format_info && format_info[:prepend_if_missing]
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
raw_serial
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_digit
|
|
55
|
+
match_group('CheckDigit')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def decode
|
|
59
|
+
decoded = {}
|
|
60
|
+
(matches.try(:names) || []).each do |name|
|
|
61
|
+
sym = name.underscore.to_sym
|
|
62
|
+
decoded[sym] = matches[name]
|
|
26
63
|
end
|
|
27
64
|
|
|
28
|
-
|
|
65
|
+
decoded
|
|
29
66
|
end
|
|
30
67
|
|
|
31
68
|
def valid?
|
|
32
69
|
return false unless valid_format?
|
|
33
70
|
return false unless valid_checksum?
|
|
34
|
-
return
|
|
71
|
+
return false unless valid_optional_checks?
|
|
72
|
+
|
|
73
|
+
true
|
|
35
74
|
end
|
|
36
75
|
|
|
37
76
|
def valid_format?
|
|
38
|
-
!matches.nil?
|
|
77
|
+
!matches.nil?
|
|
39
78
|
end
|
|
40
79
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
80
|
+
def valid_optional_checks?
|
|
81
|
+
additional_check = self.class.const_get('VALIDATION')[:additional]
|
|
82
|
+
return true unless additional_check
|
|
44
83
|
|
|
45
|
-
|
|
46
|
-
[]
|
|
84
|
+
exist_checks = (additional_check[:exists] ||= [])
|
|
85
|
+
exist_checks.all? { |w| matching_additional[w] }
|
|
47
86
|
end
|
|
48
87
|
|
|
49
88
|
def valid_checksum?
|
|
50
|
-
false
|
|
89
|
+
return false unless valid_format?
|
|
90
|
+
|
|
91
|
+
checksum_info = self.class.const_get(:VALIDATION)[:checksum]
|
|
92
|
+
return true unless checksum_info
|
|
93
|
+
|
|
94
|
+
name = checksum_info[:name]
|
|
95
|
+
method_name = "validates_#{name}?"
|
|
96
|
+
|
|
97
|
+
TrackingNumber::ChecksumValidations.send(method_name, serial_number, check_digit, checksum_info)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def checksum?
|
|
101
|
+
!!self.class.const_get(:VALIDATION)[:checksum]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
LENGTH_WEIGHT = 0.1
|
|
105
|
+
CHECKSUM_WEIGHT = 5.0
|
|
106
|
+
|
|
107
|
+
def confidence
|
|
108
|
+
(checksum? ? CHECKSUM_WEIGHT : 0) + tracking_number.length * LENGTH_WEIGHT
|
|
51
109
|
end
|
|
52
110
|
|
|
53
111
|
def to_s
|
|
54
|
-
|
|
112
|
+
tracking_number
|
|
55
113
|
end
|
|
56
114
|
|
|
57
115
|
def inspect
|
|
58
|
-
|
|
116
|
+
format('#<%s:%#0x %s%s>', self.class.to_s, object_id, tracking_number, partnership_inspect)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def partnership_inspect
|
|
120
|
+
if shipper? ^ carrier?
|
|
121
|
+
' (partnership)'
|
|
122
|
+
else
|
|
123
|
+
''
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def info
|
|
128
|
+
Info.new({
|
|
129
|
+
courier: courier_info,
|
|
130
|
+
service_type: service_type,
|
|
131
|
+
service_description: service_description,
|
|
132
|
+
destination_zip: destination_zip,
|
|
133
|
+
shipper_id: shipper_id,
|
|
134
|
+
package_type: package_type,
|
|
135
|
+
tracking_url: tracking_url,
|
|
136
|
+
partners: partners,
|
|
137
|
+
decode: decode
|
|
138
|
+
})
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def courier_code
|
|
142
|
+
self.class.const_get(:COURIER_CODE).to_sym
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def courier_name
|
|
146
|
+
if matching_additional['Courier']
|
|
147
|
+
matching_additional['Courier'][:courier]
|
|
148
|
+
elsif self.class.constants.include?(:COURIER_INFO)
|
|
149
|
+
self.class.const_get(:COURIER_INFO)[:name]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
alias carrier courier_code # OG tracking_number gem used :carrier.
|
|
154
|
+
alias carrier_code courier_code
|
|
155
|
+
alias carrier_name courier_name
|
|
156
|
+
|
|
157
|
+
def courier_info
|
|
158
|
+
basics = { name: courier_name, code: courier_code }
|
|
159
|
+
|
|
160
|
+
if info = matching_additional['Courier']
|
|
161
|
+
basics.merge!(name: info[:courier], url: info[:courier_url], country: info[:country])
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@courier ||= Info.new(basics)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def partnership?
|
|
168
|
+
partners.present?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def shipper?
|
|
172
|
+
return true unless partnership?
|
|
173
|
+
|
|
174
|
+
partners.shipper == self
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def carrier?
|
|
178
|
+
return true unless partnership?
|
|
179
|
+
|
|
180
|
+
partners.carrier == self
|
|
59
181
|
end
|
|
60
|
-
end
|
|
61
182
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
183
|
+
def partners
|
|
184
|
+
return unless self.class.const_defined?(:PARTNERS)
|
|
185
|
+
|
|
186
|
+
partner_hash = {}
|
|
187
|
+
|
|
188
|
+
return unless (partner_tn = find_matching_partner)
|
|
189
|
+
possible_twin = partner_tn.send(:find_matching_partner, self.class.const_get("ID"))
|
|
190
|
+
|
|
191
|
+
if possible_twin.instance_of?(self.class) && possible_twin.tracking_number == tracking_number
|
|
192
|
+
partner_hash[partner_data[:partner_type].to_sym] = partner_tn
|
|
193
|
+
partner_hash[partner_tn.partner_data[:partner_type].to_sym] = self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
PartnerStruct.new(partner_hash[:shipper], partner_hash[:carrier]) if partner_hash.keys.any?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def service_type
|
|
200
|
+
@service_type ||= Info.new(matching_additional['Service Type']).name if matching_additional['Service Type']
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def service_description
|
|
204
|
+
@service_description ||= Info.new(matching_additional['Service Type']).description if matching_additional['Service Type']
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def package_type
|
|
208
|
+
@package_type ||= Info.new(matching_additional['Container Type']).name if matching_additional['Container Type']
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def destination_zip
|
|
212
|
+
match_group('DestinationZip')
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def shipper_id
|
|
216
|
+
match_group('ShipperId')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def tracking_url
|
|
220
|
+
url = nil
|
|
221
|
+
if matching_additional['Courier']
|
|
222
|
+
url = matching_additional['Courier'][:tracking_url]
|
|
223
|
+
elsif self.class.const_defined?(:TRACKING_URL)
|
|
224
|
+
url = self.class.const_get(:TRACKING_URL)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
url.sub('%s', tracking_number) if url
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def matching_additional
|
|
231
|
+
additional = self.class.const_get(:ADDITIONAL) || []
|
|
232
|
+
|
|
233
|
+
relevant_sections = {}
|
|
234
|
+
|
|
235
|
+
additional.each do |additional_info|
|
|
236
|
+
next unless matches && matches.length > 0 # skip if no match groups
|
|
237
|
+
|
|
238
|
+
value = matches[additional_info[:regex_group_name]].gsub(/\s/, '') # match is empty
|
|
239
|
+
next unless value
|
|
240
|
+
next unless additional_info[:lookup]
|
|
241
|
+
|
|
242
|
+
matches = additional_info[:lookup].find do |i|
|
|
243
|
+
if i[:matches]
|
|
244
|
+
value == i[:matches]
|
|
245
|
+
elsif i[:matches_regex]
|
|
246
|
+
value =~ Regexp.new(i[:matches_regex])
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# has matching value
|
|
251
|
+
relevant_sections[additional_info[:name]] = matches
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
relevant_sections
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
protected
|
|
258
|
+
|
|
259
|
+
def match_all(items)
|
|
260
|
+
items.all? do |i|
|
|
261
|
+
if i[:matches] && matches[i[:regex_group_name]]
|
|
262
|
+
matches[i[:regex_group_name]] == i[:matches]
|
|
263
|
+
elsif i[:matches_regex] && matches[i[:regex_group_name]]
|
|
264
|
+
matches[i[:regex_group_name]] =~ Regexp.new(i[:matches_regex])
|
|
265
|
+
else
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def match_any(items)
|
|
272
|
+
items.find do |i|
|
|
273
|
+
if i[:matches] && matches[i[:regex_group_name]]
|
|
274
|
+
matches[i[:regex_group_name]] == i[:matches]
|
|
275
|
+
elsif i[:matches_regex] && matches[i[:regex_group_name]]
|
|
276
|
+
matches[i[:regex_group_name]] =~ Regexp.new(i[:matches_regex])
|
|
277
|
+
else
|
|
278
|
+
false
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def matches
|
|
284
|
+
@matches ||= if self.class.constants.include?(:VERIFY_PATTERN)
|
|
285
|
+
tracking_number.match(self.class.const_get(:VERIFY_PATTERN))
|
|
286
|
+
else
|
|
287
|
+
[]
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def match_group(name)
|
|
292
|
+
matches[name].gsub(/\s/, '')
|
|
293
|
+
rescue StandardError
|
|
294
|
+
nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def find_matching_partner(partner_id = nil)
|
|
298
|
+
partner_info = if partner_id
|
|
299
|
+
(self.class.const_get(:PARTNERS) || []).select { |info| info[:partner_id]&.to_sym == partner_id.to_sym }
|
|
300
|
+
else
|
|
301
|
+
self.class.const_get(:PARTNERS) || []
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
partner_info.each do |partner_data|
|
|
305
|
+
klass = find_tracking_class_by_id(partner_data[:partner_id])
|
|
306
|
+
|
|
307
|
+
return false unless klass
|
|
308
|
+
|
|
309
|
+
tn = klass.new(tracking_number)
|
|
310
|
+
valid = if partner_data.dig(:validation, :matches_all)
|
|
311
|
+
tn.valid? && match_all(partner_data.dig(:validation, :matches_all))
|
|
312
|
+
elsif partner_data.dig(:validation, :matches_any)
|
|
313
|
+
tn.valid? && match_any(partner_data.dig(:validation, :matches_any))
|
|
314
|
+
else
|
|
315
|
+
tn.valid?
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
next unless valid
|
|
319
|
+
|
|
320
|
+
@partner = tn
|
|
321
|
+
@partner_data = partner_data
|
|
322
|
+
break
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
return @partner
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def find_tracking_class_by_id(id)
|
|
329
|
+
return unless id
|
|
330
|
+
|
|
331
|
+
TrackingNumber::TYPES.detect do |type|
|
|
332
|
+
type.const_get('ID') == id
|
|
333
|
+
end
|
|
65
334
|
end
|
|
66
335
|
end
|
|
67
336
|
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module TrackingNumber
|
|
2
|
+
module ChecksumValidations
|
|
3
|
+
class << self
|
|
4
|
+
def validates_s10?(sequence, check_digit, extras = {})
|
|
5
|
+
weighting = [8,6,4,2,3,5,9,7]
|
|
6
|
+
|
|
7
|
+
total = 0
|
|
8
|
+
sequence.chars.to_a.zip(weighting).each do |(a,b)|
|
|
9
|
+
total += a.to_i * b.to_i
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
remainder = total % 11
|
|
13
|
+
check = case remainder
|
|
14
|
+
when 1
|
|
15
|
+
0
|
|
16
|
+
when 0
|
|
17
|
+
5
|
|
18
|
+
else
|
|
19
|
+
11 - remainder
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return check.to_i == check_digit.to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validates_sum_product_with_weightings_and_modulo?(sequence, check_digit, extras = {})
|
|
26
|
+
weighting = extras[:weightings] || []
|
|
27
|
+
|
|
28
|
+
total = 0
|
|
29
|
+
sequence.chars.to_a.zip(weighting).each do |(a,b)|
|
|
30
|
+
total += a.to_i * b
|
|
31
|
+
end
|
|
32
|
+
return (total % extras[:modulo1] % extras[:modulo2]) == check_digit.to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validates_mod10?(sequence, check_digit, extras = {})
|
|
36
|
+
total = 0
|
|
37
|
+
characters = if extras[:reverse]
|
|
38
|
+
sequence.chars.reverse
|
|
39
|
+
else
|
|
40
|
+
sequence.chars
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
characters.each_with_index do |c, i|
|
|
44
|
+
x = if c[/[0-9]/] # numeric
|
|
45
|
+
c.to_i
|
|
46
|
+
else
|
|
47
|
+
(c[0].ord - 3) % 10
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if extras[:odds_multiplier] && i.odd?
|
|
51
|
+
x *= extras[:odds_multiplier].to_i
|
|
52
|
+
elsif extras[:evens_multiplier] && i.even?
|
|
53
|
+
x *= extras[:evens_multiplier].to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
total += x
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
check = (total % 10)
|
|
60
|
+
check = (10 - check) unless (check.zero?)
|
|
61
|
+
|
|
62
|
+
return (check.to_i == check_digit.to_i)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validates_mod7?(sequence, check_digit, extras = {})
|
|
66
|
+
# standard mod 7 check
|
|
67
|
+
return true if sequence.to_i % 7 == check_digit.to_i
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validates_mod_37_36?(sequence, check_digit, extras = {})
|
|
71
|
+
# From https://esolutions.dpd.com/dokumente/DPD_Parcel_Label_Specification_2.4.1_EN.pdf
|
|
72
|
+
|
|
73
|
+
mod = 36
|
|
74
|
+
weights = {A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, G: 16, H: 17, I: 18, J: 19, K: 20, L: 21, M: 22, N: 23, O: 24, P: 25, Q: 26, R: 27, S: 28, T: 29, U: 30, V: 31, W: 32, X: 33, Y: 34, Z: 35}
|
|
75
|
+
cd = mod
|
|
76
|
+
sequence.chars.to_a.each do |char, i|
|
|
77
|
+
val = (char =~ /[A-Za-z]/ ? weights[char.to_sym] : char.to_i)
|
|
78
|
+
|
|
79
|
+
cd = val + cd
|
|
80
|
+
cd = cd - mod if cd > mod
|
|
81
|
+
cd = cd * 2
|
|
82
|
+
cd = cd - (mod + 1) if cd > mod
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
cd = (mod + 1) - cd
|
|
86
|
+
cd = 0 if cd == mod
|
|
87
|
+
computed = if cd >= 10
|
|
88
|
+
weights.find { |a, val| val == cd }[0].to_s
|
|
89
|
+
else
|
|
90
|
+
cd.to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
computed == check_digit
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validates_luhn?(sequence, check_digit, extras = {})
|
|
97
|
+
total = 0
|
|
98
|
+
sequence.chars.reverse.each_with_index do |c, i|
|
|
99
|
+
x = c.to_i
|
|
100
|
+
|
|
101
|
+
if i.even?
|
|
102
|
+
x *= 2
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if x > 9
|
|
106
|
+
x -= 9
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
total += x
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
check = (total % 10)
|
|
113
|
+
check = (10 - check) unless (check.zero?)
|
|
114
|
+
|
|
115
|
+
return (check.to_i == check_digit.to_i)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module TrackingNumber
|
|
2
|
+
class Info
|
|
3
|
+
def initialize(info_hash = {})
|
|
4
|
+
info_hash.keys.each do |key|
|
|
5
|
+
self.instance_variable_set("@#{key}", info_hash[key])
|
|
6
|
+
self.class_eval { attr_accessor key }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
if info_hash.keys.size == 1
|
|
10
|
+
@default = info_hash[info_hash.keys.first]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def method_missing(name, *args)
|
|
15
|
+
self.instance_variable_get("@#{name}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
@default || @name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_json
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module TrackingNumber
|
|
2
|
+
module Loader
|
|
3
|
+
class << self
|
|
4
|
+
def load_tracking_number_data(couriers_path = File.join(File.dirname(__FILE__), '../data/couriers/'))
|
|
5
|
+
mapping_data = []
|
|
6
|
+
tracking_number_types = []
|
|
7
|
+
Dir.glob(File.join(couriers_path, '/*.json')).each do |file|
|
|
8
|
+
courier_info = read_courier_info(file)
|
|
9
|
+
|
|
10
|
+
courier_info[:tracking_numbers].each do |tracking_info|
|
|
11
|
+
klass = create_class(courier_info, tracking_info)
|
|
12
|
+
|
|
13
|
+
# Do some basic checks on the data file
|
|
14
|
+
throw 'missing test numbers' unless has_test_numbers?(tracking_info)
|
|
15
|
+
throw 'missing regex match groups' unless test_numbers_return_required_groups?(tracking_info,
|
|
16
|
+
Regexp.new(klass::VERIFY_PATTERN))
|
|
17
|
+
const = register_class(klass, tracking_info[:name])
|
|
18
|
+
|
|
19
|
+
tracking_number_types.push(const)
|
|
20
|
+
mapping_data << {
|
|
21
|
+
class: const,
|
|
22
|
+
name: courier_info[:name],
|
|
23
|
+
courier_code: courier_info[:courier_code],
|
|
24
|
+
info: tracking_info
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
TrackingNumber.const_set('TYPES', tracking_number_types)
|
|
30
|
+
|
|
31
|
+
mapping_data
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def has_test_numbers?(tracking)
|
|
37
|
+
tracking[:test_numbers] && tracking[:test_numbers][:valid]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_numbers_return_required_groups?(tracking, regex)
|
|
41
|
+
test_number = tracking[:test_numbers][:valid][0]
|
|
42
|
+
matches = test_number.match(regex)
|
|
43
|
+
|
|
44
|
+
matches['SerialNumber']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_courier_info(file)
|
|
48
|
+
JSON.parse(File.read(file)).deep_symbolize_keys!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_class(courier_info, tracking_info)
|
|
52
|
+
klass = Class.new(TrackingNumber::Base)
|
|
53
|
+
klass.const_set('COURIER_CODE', courier_info[:courier_code])
|
|
54
|
+
info = courier_info.dup
|
|
55
|
+
info.delete(:tracking_numbers)
|
|
56
|
+
klass.const_set('COURIER_INFO', info)
|
|
57
|
+
|
|
58
|
+
pattern = tracking_info[:regex]
|
|
59
|
+
pattern = tracking_info[:regex].join if tracking_info[:regex].is_a?(Array)
|
|
60
|
+
|
|
61
|
+
verify_pattern = "^#{pattern}$"
|
|
62
|
+
search_pattern = "\\b#{pattern}\\b"
|
|
63
|
+
|
|
64
|
+
klass.const_set('SEARCH_PATTERN', Regexp.new(search_pattern))
|
|
65
|
+
klass.const_set('VERIFY_PATTERN', Regexp.new(verify_pattern))
|
|
66
|
+
|
|
67
|
+
klass.const_set('VALIDATION', tracking_info[:validation])
|
|
68
|
+
klass.const_set('ADDITIONAL', tracking_info[:additional])
|
|
69
|
+
klass.const_set('TRACKING_URL', tracking_info[:tracking_url])
|
|
70
|
+
klass.const_set('PARTNERS', tracking_info[:partners])
|
|
71
|
+
klass.const_set('ID', tracking_info[:id])
|
|
72
|
+
|
|
73
|
+
klass
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def register_class(klass, tracking_name)
|
|
77
|
+
klass_name = tracking_name.gsub(/[^0-9A-Za-z]/, '')
|
|
78
|
+
TrackingNumber.const_set(klass_name, klass)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module TrackingNumber
|
|
2
|
+
class Unknown < Base
|
|
3
|
+
COURIER_CODE = :unknown
|
|
4
|
+
|
|
5
|
+
def carrier
|
|
6
|
+
COURIER_CODE
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def courier_name
|
|
10
|
+
"Unknown"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid?
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid_format?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def valid_checksum?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def shipper?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def carrier?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def partnership?
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def decode
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def matching_additional
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|