tracking_number 1.3.5 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +48 -0
- data/bin/console +14 -0
- data/lib/data/couriers/amazon.json +3 -1
- data/lib/data/couriers/canadapost.json +1 -0
- data/lib/data/couriers/dhl.json +8 -23
- data/lib/data/couriers/dpd.json +2 -0
- data/lib/data/couriers/fedex.json +29 -5
- data/lib/data/couriers/landmark.json +1 -0
- data/lib/data/couriers/lasership.json +2 -0
- data/lib/data/couriers/ontrac.json +4 -1
- data/lib/data/couriers/s10.json +2 -2
- data/lib/data/couriers/ups.json +3 -1
- data/lib/data/couriers/usps.json +37 -2
- data/lib/tracking_number/base.rb +166 -85
- data/lib/tracking_number/loader.rb +23 -22
- data/lib/tracking_number/partnership.rb +7 -0
- data/lib/tracking_number/version.rb +1 -1
- data/lib/tracking_number.rb +32 -9
- data/test/test_helper.rb +0 -2
- data/test/tracking_number_meta_test.rb +1 -1
- data/test/tracking_number_test.rb +68 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 632e88e03ffd1404e26689c874e6093d8b978ceb7326d0e9c13879ffcf5c4a27
|
4
|
+
data.tar.gz: eb6acde6415fb26c3e303ba155714b8f033a14108b4b39a123f841b38d0310ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5816c80bcd06b12d4918ca9b8b017035eb9f8caa2fb79c47a457e92863636d2263aa2f09abe757fe5219d149eb940a67556f4efcfa43501cdbb47ef304ee743a
|
7
|
+
data.tar.gz: 416f4a346ad8449e39d0bb570fbbd9ea157c19062f32d7ffc4cc8f2514a5e75b04ee8fa4715ac6a1fc2667de4f8264f8763b43a249a88357a2c50b1eb99c6333
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
tracking_number changelog
|
2
2
|
|
3
|
+
# [1.4.0](https://github.com/jkeen/tracking_number/compare/v1.3.5...v1.4.0) (2023-05-15)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* add partnership support with tracking_number_data 1.5 ([75fe530](https://github.com/jkeen/tracking_number/commit/75fe5304e933679aff89a91a93d2b7cf58c1fe5c))
|
9
|
+
* update to tracking number data 1.5 ([237ad3c](https://github.com/jkeen/tracking_number/commit/237ad3c82230d39b415df56eb8c8c9eeb22ba15c))
|
10
|
+
|
3
11
|
## [1.3.5](https://github.com/jkeen/tracking_number/compare/v1.3.4...v1.3.5) (2023-01-09)
|
4
12
|
|
5
13
|
|
data/README.md
CHANGED
@@ -92,6 +92,28 @@ t = TrackingNumber.new("012345000000002")
|
|
92
92
|
t.package_type #=> "case/carton"
|
93
93
|
```
|
94
94
|
|
95
|
+
#### Tracking URL
|
96
|
+
Get the tracking url from the shipper
|
97
|
+
```ruby
|
98
|
+
t = TrackingNumber.new("1Z6072AF0320751583")
|
99
|
+
t.tracking_url #=> ""https://wwwapps.ups.com/WebTracking/track?track=yes&trackNums=1Z6072AF0320751583"
|
100
|
+
```
|
101
|
+
|
102
|
+
#### All the info we have
|
103
|
+
Get a object of all the info we have on the thing
|
104
|
+
```ruby
|
105
|
+
t = TrackingNumber.new("1Z6072AF0320751583")
|
106
|
+
t.info #=> @courier=#<TrackingNumber::Info:0x000000010a45fed8 @code=:ups, @name="UPS">,
|
107
|
+
# @decode={:serial_number=>"6072AF032075158", :shipper_id=>"6072AF", :service_type=>"03", :package_id=>"2075158", :check_digit=>"3"},
|
108
|
+
# @destination_zip=nil,
|
109
|
+
# @package_type=nil,
|
110
|
+
# @partners=nil,
|
111
|
+
# @service_description=nil,
|
112
|
+
# @service_type="UPS United States Ground",
|
113
|
+
# @shipper_id="6072AF",
|
114
|
+
# @tracking_url="https://wwwapps.ups.com/WebTracking/track?track=yes&trackNums=1Z6072AF0320751583">
|
115
|
+
```
|
116
|
+
|
95
117
|
#### Decoding
|
96
118
|
Most tracking numbers have a format where each part of the number has meaning. `decode` splits up the number into its known named parts.
|
97
119
|
```ruby
|
@@ -107,6 +129,32 @@ Most tracking numbers have a format where each part of the number has meaning. `
|
|
107
129
|
# }
|
108
130
|
```
|
109
131
|
|
132
|
+
#### Multiple shippers / Partnerships
|
133
|
+
Some tracking numbers match multiple carriers, because they belong to multiple carriers. Some shipments like Fedex Smartpost contract the "last mile" out to USPS.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# Search defaults to only showing numbers that fulfill the carrier side of the relationship
|
137
|
+
# (if a partnership exists at all), as this is the end a consumer would most likely be interested in.
|
138
|
+
|
139
|
+
results = TrackingNumber.search('420112139261290983497923666238')
|
140
|
+
=> [#<TrackingNumber::USPS91:0x26ac0 420112139261290983497923666238>]
|
141
|
+
|
142
|
+
all_results = TrackingNumber.search('420112139261290983497923666238', match: :all)
|
143
|
+
=> [#<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>, #<TrackingNumber::USPS91:0x26ac0 420112139261290983497923666238>]
|
144
|
+
|
145
|
+
tn = results.first
|
146
|
+
tn.shipper? #=> false
|
147
|
+
tn.carrier? #=> true
|
148
|
+
tn.partnership? #=> true
|
149
|
+
tn.partners
|
150
|
+
#=> <struct TrackingNumber::Base::PartnerStruct
|
151
|
+
# shipper=#<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>,
|
152
|
+
# carrier=#<TrackingNumber::USPS91:0x2f1fc 420112139261290983497923666238>>
|
153
|
+
|
154
|
+
tn.partners.shipper #=> #<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>
|
155
|
+
tn.partners.carrier == tn #=> true
|
156
|
+
```
|
157
|
+
|
110
158
|
## ActiveModel validation
|
111
159
|
|
112
160
|
For Rails 3 (or any ActiveModel client), validate your fields as a tracking number:
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tracking_number'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "Amazon Logistics",
|
7
|
+
"id": "amazon_logistics",
|
7
8
|
"regex": [
|
8
9
|
"\\s*T\\s*B\\s*A\\s*(?<SerialNumber>([0-9]\\s*){12,12})\\s*"
|
9
10
|
],
|
@@ -12,7 +13,7 @@
|
|
12
13
|
"valid": [
|
13
14
|
"TBA000000000000",
|
14
15
|
"TBA010000000000",
|
15
|
-
"
|
16
|
+
"TBA 000000000000",
|
16
17
|
"TBA502887274000"
|
17
18
|
],
|
18
19
|
"invalid": [
|
@@ -25,6 +26,7 @@
|
|
25
26
|
},
|
26
27
|
{
|
27
28
|
"name": "Amazon International",
|
29
|
+
"id": "amazon_international",
|
28
30
|
"regex": [
|
29
31
|
"\\s*[AFC]\\s*(?<SerialNumber>([0-9]\\s*){10,10})\\s*"
|
30
32
|
],
|
data/lib/data/couriers/dhl.json
CHANGED
@@ -4,7 +4,8 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "DHL Express",
|
7
|
-
"
|
7
|
+
"id": "dhl_express",
|
8
|
+
"regex": "\\s*(?<SerialNumber>(J[A-Z][A-Z][A-Z])?([0-9])?([0-9]\\s*){9})(?<CheckDigit>([0-9]\\s*))",
|
8
9
|
"validation": {
|
9
10
|
"checksum": {
|
10
11
|
"name": "mod7"
|
@@ -13,47 +14,31 @@
|
|
13
14
|
"tracking_url": "http://www.dhl.com/en/express/tracking.html?brand=DHL&AWB=%s",
|
14
15
|
"test_numbers": {
|
15
16
|
"valid": [
|
17
|
+
"JVGL0999999990",
|
16
18
|
"3318810025",
|
19
|
+
"73891051146",
|
17
20
|
"8487135506",
|
18
21
|
"3318810036",
|
19
|
-
" 3 3 1 8 8 1 0 0 3 6 ",
|
20
22
|
"3318810014"
|
21
23
|
],
|
22
24
|
"invalid": [
|
23
25
|
"3318810010",
|
24
26
|
"3318810034",
|
27
|
+
"JVGL3099999999",
|
25
28
|
"3318810011"
|
26
29
|
]
|
27
30
|
}
|
28
31
|
},
|
29
|
-
{
|
30
|
-
"name": "DHL Express Air",
|
31
|
-
"regex": "\\s*(?<SerialNumber>([0-9]\\s*){10})(?<CheckDigit>[0-9]\\s*)",
|
32
|
-
"validation": {
|
33
|
-
"checksum": {
|
34
|
-
"name": "mod7"
|
35
|
-
}
|
36
|
-
},
|
37
|
-
"tracking_url": "http://www.dhl.com/en/express/tracking.html?brand=DHL&AWB=%s",
|
38
|
-
"test_numbers": {
|
39
|
-
"valid": [
|
40
|
-
"73891051146",
|
41
|
-
" 7 3 8 9 1 0 5 1 1 4 6 "
|
42
|
-
],
|
43
|
-
"invalid": [
|
44
|
-
"73891051149",
|
45
|
-
"73891051147"
|
46
|
-
]
|
47
|
-
}
|
48
|
-
},
|
49
32
|
{
|
50
33
|
"name": "DHL E-Commerce",
|
51
|
-
"
|
34
|
+
"id": "dhl_ecommerce",
|
35
|
+
"regex": "\\s*((GM)|(LX)|(RX)|(UV)|(CN)|(SG)|(TH)|(IN)|(HK)|(MY))\\s*(?<SerialNumber>([0-9]\\s*){10,39})",
|
52
36
|
"validation": {},
|
53
37
|
"tracking_url": "http://www.dhl.com/en/express/tracking.html?brand=DHL&AWB=%s",
|
54
38
|
"test_numbers": {
|
55
39
|
"valid": [
|
56
40
|
"GM2951173225174494",
|
41
|
+
"GM 2 9 5 117 32 25 1 7 44 9 4",
|
57
42
|
"GM295117494011169042"
|
58
43
|
],
|
59
44
|
"invalid": [
|
data/lib/data/couriers/dpd.json
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "DPD (28)",
|
7
|
+
"id": "dpd",
|
7
8
|
"regex": [
|
8
9
|
"\\s*",
|
9
10
|
"(?<SerialNumber>",
|
@@ -313,6 +314,7 @@
|
|
313
314
|
},
|
314
315
|
{
|
315
316
|
"name": "DPD (14)",
|
317
|
+
"id": "dpd_14",
|
316
318
|
"regex": [
|
317
319
|
"\\s*(?<SerialNumber>",
|
318
320
|
"([0-9]\\s*){14}",
|
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "FedEx Express (12)",
|
7
|
+
"id": "fedex_12",
|
7
8
|
"regex": "\\s*(?<SerialNumber>([0-9]\\s*){11})(?<CheckDigit>[0-9]\\s*)",
|
8
9
|
"validation": {
|
9
10
|
"checksum": {
|
@@ -42,6 +43,7 @@
|
|
42
43
|
},
|
43
44
|
{
|
44
45
|
"name": "FedEx Express (34)",
|
46
|
+
"id": "fedex_34",
|
45
47
|
"regex": [
|
46
48
|
"\\s*1\\s*0\\s*[0-9]\\s*[0-9]\\s*[0-9]\\s*",
|
47
49
|
"([0-9]\\s*){10}",
|
@@ -88,19 +90,38 @@
|
|
88
90
|
},
|
89
91
|
{
|
90
92
|
"name": "FedEx SmartPost",
|
91
|
-
"
|
93
|
+
"id": "fedex_smartpost",
|
94
|
+
"description": "Shipped by FedEx, Delivered by USPS",
|
92
95
|
"regex": [
|
93
96
|
"\\s*(?:",
|
94
97
|
"(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
|
95
98
|
"(?<ApplicationIdentifier>9\\s*2\\s*)",
|
96
99
|
")?",
|
97
100
|
"(?<SerialNumber>",
|
98
|
-
"(?<
|
99
|
-
"(?<
|
100
|
-
"(?<
|
101
|
+
"(?<SCNC>([0-9]\\s*){2})",
|
102
|
+
"(?<ServiceType>([0-9]\\s*){2})",
|
103
|
+
"(?<ShipperId>([0-9]\\s*){8})",
|
104
|
+
"(?<PackageId>([0-9]\\s*){11}|([0-9]\\s*){7})",
|
101
105
|
")",
|
102
106
|
"(?<CheckDigit>([0-9]\\s*))"
|
103
107
|
],
|
108
|
+
"additional": [
|
109
|
+
{
|
110
|
+
"name": "Service Type",
|
111
|
+
"regex_group_name": "ServiceType",
|
112
|
+
"lookup": [
|
113
|
+
{
|
114
|
+
"matches_regex": ".",
|
115
|
+
"name": "Delivered by USPS"
|
116
|
+
}
|
117
|
+
]
|
118
|
+
}
|
119
|
+
],
|
120
|
+
"partners": [{
|
121
|
+
"partner_id": "usps_91",
|
122
|
+
"partner_type": "carrier",
|
123
|
+
"description": "FedEx SmartPost is a shipping service that utilizes FedEx for the initial transport and the United States Postal Service for final delivery."
|
124
|
+
}],
|
104
125
|
"validation": {
|
105
126
|
"checksum": {
|
106
127
|
"name": "mod10",
|
@@ -118,7 +139,6 @@
|
|
118
139
|
"test_numbers": {
|
119
140
|
"valid": [
|
120
141
|
"61299998820821171811",
|
121
|
-
" 6 1 2 9 9 9 9 8 8 2 0 8 2 1 1 7 1 8 1 1 ",
|
122
142
|
"9261292700768711948021",
|
123
143
|
"420 11213 92 6129098349792366623 8",
|
124
144
|
"92 6129098349792366623 8",
|
@@ -134,6 +154,7 @@
|
|
134
154
|
},
|
135
155
|
{
|
136
156
|
"name": "FedEx Ground",
|
157
|
+
"id": "fedex_ground",
|
137
158
|
"regex": "\\s*(?<SerialNumber>([0-9]\\s*){14})(?<CheckDigit>([0-9]\\s*))",
|
138
159
|
"validation": {
|
139
160
|
"checksum": {
|
@@ -157,6 +178,7 @@
|
|
157
178
|
},
|
158
179
|
{
|
159
180
|
"name": "FedEx Ground (SSCC-18)",
|
181
|
+
"id": "fedex_ground_sscc_18",
|
160
182
|
"regex": "\\s*(?<ShippingContainerType>([0-9]\\s*){2})(?<SerialNumber>([0-9]\\s*){15})(?<CheckDigit>[0-9]\\s*)",
|
161
183
|
"tracking_url": "https://www.fedex.com/apps/fedextrack/?tracknumbers=%s",
|
162
184
|
"validation": {
|
@@ -202,6 +224,7 @@
|
|
202
224
|
},
|
203
225
|
{
|
204
226
|
"name": "FedEx Ground 96 (22)",
|
227
|
+
"id": "fedex_ground_96",
|
205
228
|
"regex": [
|
206
229
|
"\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
|
207
230
|
"(?<SCNC>([0-9]\\s*){2})",
|
@@ -229,6 +252,7 @@
|
|
229
252
|
},
|
230
253
|
{
|
231
254
|
"name": "FedEx Ground GSN",
|
255
|
+
"id": "fedex_ground_gsn",
|
232
256
|
"regex": [
|
233
257
|
"\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
|
234
258
|
"(?<SCNC>([0-9]\\s*){2})",
|
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "LaserShip LX",
|
7
|
+
"id": "lasership_lx",
|
7
8
|
"regex": [
|
8
9
|
"\\s*L\\s*[AIEHNX]\\s*[1-3]\\s*(?<SerialNumber>([0-9]\\s*){7,7})\\s*"
|
9
10
|
],
|
@@ -42,6 +43,7 @@
|
|
42
43
|
},
|
43
44
|
{
|
44
45
|
"name": "LaserShip 1LS7 (15)",
|
46
|
+
"id": "lasership_1ls7",
|
45
47
|
"regex": [
|
46
48
|
"\\s*1\\s*L\\s*S\\s*7\\s*[12]\\s*([0-9]\\s*){4,4}",
|
47
49
|
"(?<SerialNumber>([0-9]\\s*){6,6})\\s*"
|
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "OnTrac",
|
7
|
+
"id": "ontrac_c",
|
7
8
|
"regex": "\\s*C\\s*(?<SerialNumber>([0-9]\\s*){13})(?<CheckDigit>[0-9]\\s*)",
|
8
9
|
"validation": {
|
9
10
|
"checksum": {
|
@@ -22,7 +23,7 @@
|
|
22
23
|
"test_numbers": {
|
23
24
|
"valid": [
|
24
25
|
"C11031500001879",
|
25
|
-
"
|
26
|
+
"C 110 31 500 00187 9",
|
26
27
|
"C10999911320231",
|
27
28
|
"C11121552953069",
|
28
29
|
"C11121553156000",
|
@@ -36,6 +37,7 @@
|
|
36
37
|
},
|
37
38
|
{
|
38
39
|
"name": "OnTrac D",
|
40
|
+
"id": "ontrac_d",
|
39
41
|
"regex": "\\s*D\\s*(?<SerialNumber>([0-9]\\s*){13})(?<CheckDigit>[0-9]\\s*)",
|
40
42
|
"validation": {
|
41
43
|
"checksum": {
|
@@ -55,6 +57,7 @@
|
|
55
57
|
"valid": [
|
56
58
|
"D10011354453707",
|
57
59
|
"D10011345983010",
|
60
|
+
"D 100 113 459 830 10",
|
58
61
|
"D10011342332145"
|
59
62
|
],
|
60
63
|
"invalid": [
|
data/lib/data/couriers/s10.json
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
"courier_code": "s10",
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
|
+
"id": "s10",
|
6
7
|
"name": "S10",
|
7
8
|
"validation": {
|
8
9
|
"checksum": {
|
@@ -21,8 +22,7 @@
|
|
21
22
|
"RB123456785GB",
|
22
23
|
"RB123456785US",
|
23
24
|
"RB123456785CV",
|
24
|
-
"RB123456785CF"
|
25
|
-
" R B 1 2 3 4 5 6 7 8 5 C F "
|
25
|
+
"RB123456785CF"
|
26
26
|
],
|
27
27
|
"invalid": [
|
28
28
|
"RB123456786US",
|
data/lib/data/couriers/ups.json
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
"tracking_numbers": [
|
5
5
|
{
|
6
6
|
"name": "UPS",
|
7
|
+
"id": "ups",
|
7
8
|
"regex": [
|
8
9
|
"\\s*1\\s*Z\\s*(?<SerialNumber>",
|
9
10
|
"(?<ShipperId>(?:[A-Z0-9]\\s*){6,6})",
|
@@ -138,7 +139,7 @@
|
|
138
139
|
{
|
139
140
|
"name": "UPS Waybill",
|
140
141
|
"regex": [
|
141
|
-
"\\s*(?<ServiceType>([
|
142
|
+
"\\s*(?<ServiceType>([AHJKTV]\\s*){1})",
|
142
143
|
"(?<SerialNumber>(?:[0-9]\\s*){9})",
|
143
144
|
"(?<CheckDigit>[0-9]\\s*){1}"
|
144
145
|
],
|
@@ -173,6 +174,7 @@
|
|
173
174
|
"test_numbers": {
|
174
175
|
"valid": [
|
175
176
|
"K1506235620",
|
177
|
+
"K 150 623 562 0",
|
176
178
|
"K2479825491",
|
177
179
|
"J4603636537",
|
178
180
|
"V0490119172",
|
data/lib/data/couriers/usps.json
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
{
|
6
6
|
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
|
7
7
|
"name": "USPS 20",
|
8
|
+
"id": "usps_20",
|
8
9
|
"description": "20 digit USPS numbers",
|
9
10
|
"regex": [
|
10
11
|
"\\s*(?<SerialNumber>",
|
@@ -58,6 +59,7 @@
|
|
58
59
|
},
|
59
60
|
{
|
60
61
|
"name": "USPS 34v2",
|
62
|
+
"id": "usps_32V2",
|
61
63
|
"description": "variation on 34 digit USPS IMpd numbers",
|
62
64
|
"regex": [
|
63
65
|
"\\s*(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})",
|
@@ -90,6 +92,7 @@
|
|
90
92
|
},
|
91
93
|
{
|
92
94
|
"name": "USPS 91",
|
95
|
+
"id": "usps_91",
|
93
96
|
"description": "USPS now calls this the IMpd barcode format",
|
94
97
|
"regex": [
|
95
98
|
"\\s*(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
|
@@ -115,6 +118,23 @@
|
|
115
118
|
}
|
116
119
|
}
|
117
120
|
},
|
121
|
+
"partners": [{
|
122
|
+
"description": "FedEx SmartPost uses USPS for last mile delivery, but not all USPS91 numbers are SmartPosts",
|
123
|
+
"partner_type": "shipper",
|
124
|
+
"partner_id": "fedex_smartpost",
|
125
|
+
"validation": {
|
126
|
+
"matches_all": [
|
127
|
+
{
|
128
|
+
"regex_group_name": "ServiceType",
|
129
|
+
"matches": "29"
|
130
|
+
},
|
131
|
+
{
|
132
|
+
"matches": "61",
|
133
|
+
"regex_group_name": "SCNC"
|
134
|
+
}
|
135
|
+
]
|
136
|
+
}
|
137
|
+
}],
|
118
138
|
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
|
119
139
|
"test_numbers": {
|
120
140
|
"valid": [
|
@@ -125,7 +145,6 @@
|
|
125
145
|
"92748931507708513018050063",
|
126
146
|
"9400 1112 0108 0805 4830 16",
|
127
147
|
"9361 2898 7870 0317 6337 95",
|
128
|
-
" 9 3 6 1 2 8 9 8 7 8 7 0 0 3 1 7 6 3 3 7 9 5 ",
|
129
148
|
"9405803699300124287899"
|
130
149
|
],
|
131
150
|
"invalid": [
|
@@ -134,7 +153,23 @@
|
|
134
153
|
"420000000000000000000000000000",
|
135
154
|
"420000009200000000000000000000"
|
136
155
|
]
|
137
|
-
}
|
156
|
+
},
|
157
|
+
"additional": [
|
158
|
+
{
|
159
|
+
"name": "Service Type",
|
160
|
+
"regex_group_name": "ServiceType",
|
161
|
+
"lookup": [
|
162
|
+
{
|
163
|
+
"matches": "11",
|
164
|
+
"name": "First Class (R)"
|
165
|
+
},
|
166
|
+
{
|
167
|
+
"matches": "29",
|
168
|
+
"name": "Fedex Smart Post"
|
169
|
+
}
|
170
|
+
]
|
171
|
+
}
|
172
|
+
]
|
138
173
|
}
|
139
174
|
]
|
140
175
|
}
|
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|
|
@@ -23,10 +24,10 @@ module TrackingNumber
|
|
23
24
|
# matches with match groups within the match data
|
24
25
|
matches = []
|
25
26
|
|
26
|
-
body.scan(
|
27
|
-
#get the match data instead, which is needed with these types of regexes
|
27
|
+
body.scan(const_get(:SEARCH_PATTERN)) do
|
28
|
+
# get the match data instead, which is needed with these types of regexes
|
28
29
|
matches << $~
|
29
|
-
|
30
|
+
end
|
30
31
|
|
31
32
|
if matches
|
32
33
|
matches.collect { |m| m[0] }
|
@@ -36,31 +37,29 @@ module TrackingNumber
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def serial_number
|
39
|
-
return match_group(
|
40
|
+
return match_group('SerialNumber') unless self.class.const_get('VALIDATION')
|
40
41
|
|
41
42
|
format_info = self.class.const_get(:VALIDATION)[:serial_number_format]
|
42
|
-
raw_serial = match_group(
|
43
|
+
raw_serial = match_group('SerialNumber')
|
43
44
|
|
44
|
-
if format_info
|
45
|
-
|
46
|
-
|
47
|
-
elsif format_info[:prepend_if_missing]
|
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
48
|
|
49
|
-
end
|
50
49
|
end
|
51
50
|
|
52
|
-
|
51
|
+
raw_serial
|
53
52
|
end
|
54
53
|
|
55
54
|
def check_digit
|
56
|
-
match_group(
|
55
|
+
match_group('CheckDigit')
|
57
56
|
end
|
58
57
|
|
59
58
|
def decode
|
60
59
|
decoded = {}
|
61
|
-
(
|
60
|
+
(matches.try(:names) || []).each do |name|
|
62
61
|
sym = name.underscore.to_sym
|
63
|
-
decoded[sym] =
|
62
|
+
decoded[sym] = matches[name]
|
64
63
|
end
|
65
64
|
|
66
65
|
decoded
|
@@ -70,7 +69,8 @@ module TrackingNumber
|
|
70
69
|
return false unless valid_format?
|
71
70
|
return false unless valid_checksum?
|
72
71
|
return false unless valid_optional_checks?
|
73
|
-
|
72
|
+
|
73
|
+
true
|
74
74
|
end
|
75
75
|
|
76
76
|
def valid_format?
|
@@ -78,7 +78,7 @@ module TrackingNumber
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def valid_optional_checks?
|
81
|
-
additional_check = self.class.const_get(
|
81
|
+
additional_check = self.class.const_get('VALIDATION')[:additional]
|
82
82
|
return true unless additional_check
|
83
83
|
|
84
84
|
exist_checks = (additional_check[:exists] ||= [])
|
@@ -86,8 +86,9 @@ module TrackingNumber
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def valid_checksum?
|
89
|
-
return false unless
|
90
|
-
|
89
|
+
return false unless valid_format?
|
90
|
+
|
91
|
+
checksum_info = self.class.const_get(:VALIDATION)[:checksum]
|
91
92
|
return true unless checksum_info
|
92
93
|
|
93
94
|
name = checksum_info[:name]
|
@@ -97,22 +98,25 @@ module TrackingNumber
|
|
97
98
|
end
|
98
99
|
|
99
100
|
def to_s
|
100
|
-
|
101
|
+
tracking_number
|
101
102
|
end
|
102
103
|
|
103
104
|
def inspect
|
104
|
-
|
105
|
+
format('#<%s:%#0x %s>', self.class.to_s, object_id, tracking_number)
|
105
106
|
end
|
106
107
|
|
107
108
|
def info
|
108
109
|
Info.new({
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
110
|
+
courier: courier_info,
|
111
|
+
service_type: service_type,
|
112
|
+
service_description: service_description,
|
113
|
+
destination_zip: destination_zip,
|
114
|
+
shipper_id: shipper_id,
|
115
|
+
package_type: package_type,
|
116
|
+
tracking_url: tracking_url,
|
117
|
+
partners: partners,
|
118
|
+
decode: decode
|
119
|
+
})
|
116
120
|
end
|
117
121
|
|
118
122
|
def courier_code
|
@@ -120,68 +124,88 @@ module TrackingNumber
|
|
120
124
|
end
|
121
125
|
|
122
126
|
def courier_name
|
123
|
-
if matching_additional[
|
124
|
-
matching_additional[
|
125
|
-
|
126
|
-
|
127
|
-
self.class.const_get(:COURIER_INFO)[:name]
|
128
|
-
end
|
127
|
+
if matching_additional['Courier']
|
128
|
+
matching_additional['Courier'][:courier]
|
129
|
+
elsif self.class.constants.include?(:COURIER_INFO)
|
130
|
+
self.class.const_get(:COURIER_INFO)[:name]
|
129
131
|
end
|
130
132
|
end
|
131
133
|
|
132
|
-
|
133
|
-
|
134
|
-
|
134
|
+
alias carrier courier_code # OG tracking_number gem used :carrier.
|
135
|
+
alias carrier_code courier_code
|
136
|
+
alias carrier_name courier_name
|
135
137
|
|
136
138
|
def courier_info
|
137
|
-
basics = {:
|
139
|
+
basics = { name: courier_name, code: courier_code }
|
138
140
|
|
139
|
-
if info = matching_additional[
|
140
|
-
basics.merge!(:
|
141
|
+
if info = matching_additional['Courier']
|
142
|
+
basics.merge!(name: info[:courier], url: info[:courier_url], country: info[:country])
|
141
143
|
end
|
142
144
|
|
143
145
|
@courier ||= Info.new(basics)
|
144
146
|
end
|
145
147
|
|
146
|
-
def
|
147
|
-
|
148
|
-
|
148
|
+
def partnership?
|
149
|
+
partners.present?
|
150
|
+
end
|
151
|
+
|
152
|
+
def shipper?
|
153
|
+
return true unless partnership?
|
154
|
+
|
155
|
+
partners.shipper == self
|
156
|
+
end
|
157
|
+
|
158
|
+
def carrier?
|
159
|
+
return true unless partnership?
|
160
|
+
|
161
|
+
partners.carrier == self
|
162
|
+
end
|
163
|
+
|
164
|
+
def partners
|
165
|
+
return unless self.class.const_defined?(:PARTNERS)
|
166
|
+
|
167
|
+
partner_hash = {}
|
168
|
+
|
169
|
+
return unless (partner_tn = find_matching_partner)
|
170
|
+
|
171
|
+
possible_twin = partner_tn.send(:find_matching_partner)
|
172
|
+
if possible_twin.instance_of?(self.class) && possible_twin.tracking_number == tracking_number
|
173
|
+
partner_hash[partner_data[:partner_type].to_sym] = partner_tn
|
174
|
+
partner_hash[partner_tn.partner_data[:partner_type].to_sym] = self
|
149
175
|
end
|
176
|
+
|
177
|
+
PartnerStruct.new(partner_hash[:shipper], partner_hash[:carrier]) if partner_hash.keys.any?
|
178
|
+
end
|
179
|
+
|
180
|
+
def service_type
|
181
|
+
@service_type ||= Info.new(matching_additional['Service Type']).name if matching_additional['Service Type']
|
150
182
|
end
|
151
183
|
|
152
184
|
def service_description
|
153
|
-
if matching_additional[
|
154
|
-
@service_description ||= Info.new(matching_additional["Service Type"]).description
|
155
|
-
end
|
185
|
+
@service_description ||= Info.new(matching_additional['Service Type']).description if matching_additional['Service Type']
|
156
186
|
end
|
157
187
|
|
158
188
|
def package_type
|
159
|
-
if matching_additional[
|
160
|
-
@package_type ||= Info.new(matching_additional["Container Type"]).name
|
161
|
-
end
|
189
|
+
@package_type ||= Info.new(matching_additional['Container Type']).name if matching_additional['Container Type']
|
162
190
|
end
|
163
191
|
|
164
192
|
def destination_zip
|
165
|
-
match_group(
|
193
|
+
match_group('DestinationZip')
|
166
194
|
end
|
167
195
|
|
168
196
|
def shipper_id
|
169
|
-
match_group(
|
197
|
+
match_group('ShipperId')
|
170
198
|
end
|
171
199
|
|
172
200
|
def tracking_url
|
173
201
|
url = nil
|
174
|
-
if matching_additional[
|
175
|
-
url = matching_additional[
|
176
|
-
|
177
|
-
|
178
|
-
url = self.class.const_get(:TRACKING_URL)
|
179
|
-
end
|
202
|
+
if matching_additional['Courier']
|
203
|
+
url = matching_additional['Courier'][:tracking_url]
|
204
|
+
elsif self.class.const_defined?(:TRACKING_URL)
|
205
|
+
url = self.class.const_get(:TRACKING_URL)
|
180
206
|
end
|
181
207
|
|
182
|
-
if url
|
183
|
-
url.sub('%s', self.tracking_number)
|
184
|
-
end
|
208
|
+
url.sub('%s', tracking_number) if url
|
185
209
|
end
|
186
210
|
|
187
211
|
def matching_additional
|
@@ -189,21 +213,21 @@ module TrackingNumber
|
|
189
213
|
|
190
214
|
relevant_sections = {}
|
191
215
|
|
192
|
-
additional.each do |
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
end
|
203
|
-
|
204
|
-
relevant_sections[info[:name]] = matches
|
216
|
+
additional.each do |additional_info|
|
217
|
+
next unless matches && matches.length > 0 # skip if no match groups
|
218
|
+
value = matches[additional_info[:regex_group_name]].gsub(/\s/, '') # match is empty
|
219
|
+
next unless value
|
220
|
+
|
221
|
+
matches = additional_info[:lookup].find do |i|
|
222
|
+
if i[:matches]
|
223
|
+
value == i[:matches]
|
224
|
+
elsif i[:matches_regex]
|
225
|
+
value =~ Regexp.new(i[:matches_regex])
|
205
226
|
end
|
206
227
|
end
|
228
|
+
|
229
|
+
# has matching value
|
230
|
+
relevant_sections[additional_info[:name]] = matches
|
207
231
|
end
|
208
232
|
|
209
233
|
relevant_sections
|
@@ -211,21 +235,78 @@ module TrackingNumber
|
|
211
235
|
|
212
236
|
protected
|
213
237
|
|
214
|
-
def
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
[]
|
238
|
+
def match_all(items)
|
239
|
+
items.all? do |i|
|
240
|
+
if i[:matches] && matches[i[:regex_group_name]]
|
241
|
+
matches[i[:regex_group_name]] == i[:matches]
|
242
|
+
elsif i[:matches_regex] && matches[i[:regex_group_name]]
|
243
|
+
matches[i[:regex_group_name]] =~ Regexp.new(i[:matches_regex])
|
244
|
+
else
|
245
|
+
false
|
246
|
+
end
|
219
247
|
end
|
220
248
|
end
|
221
249
|
|
250
|
+
def match_any(items)
|
251
|
+
items.find do |i|
|
252
|
+
if i[:matches] && matches[i[:regex_group_name]]
|
253
|
+
matches[i[:regex_group_name]] == i[:matches]
|
254
|
+
elsif i[:matches_regex] && matches[i[:regex_group_name]]
|
255
|
+
matches[i[:regex_group_name]] =~ Regexp.new(i[:matches_regex])
|
256
|
+
else
|
257
|
+
false
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def matches
|
263
|
+
@matches ||= if self.class.constants.include?(:VERIFY_PATTERN)
|
264
|
+
tracking_number.match(self.class.const_get(:VERIFY_PATTERN))
|
265
|
+
else
|
266
|
+
[]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
222
270
|
def match_group(name)
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
271
|
+
matches[name].gsub(/\s/, '')
|
272
|
+
rescue StandardError
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
|
276
|
+
def find_matching_partner
|
277
|
+
partner_info = self.class.const_get(:PARTNERS) || []
|
278
|
+
|
279
|
+
partner_info.each do |partner_data|
|
280
|
+
klass = find_tracking_class_by_id(partner_data[:partner_id])
|
281
|
+
|
282
|
+
return false unless klass
|
283
|
+
|
284
|
+
tn = klass.new(tracking_number)
|
285
|
+
|
286
|
+
valid = if partner_data.dig(:validation, :matches_all)
|
287
|
+
tn.valid? && match_all(partner_data.dig(:validation, :matches_all))
|
288
|
+
elsif partner_data.dig(:validation, :matches_any)
|
289
|
+
tn.valid? && match_any(partner_data.dig(:validation, :matches_any))
|
290
|
+
else
|
291
|
+
tn.valid?
|
292
|
+
end
|
293
|
+
|
294
|
+
next unless valid
|
295
|
+
|
296
|
+
@partner = tn
|
297
|
+
@partner_data = partner_data
|
298
|
+
break
|
227
299
|
end
|
300
|
+
|
301
|
+
return @partner
|
228
302
|
end
|
229
303
|
|
304
|
+
def find_tracking_class_by_id(id)
|
305
|
+
return unless id
|
306
|
+
|
307
|
+
TrackingNumber::TYPES.detect do |type|
|
308
|
+
type.const_get('ID') == id
|
309
|
+
end
|
310
|
+
end
|
230
311
|
end
|
231
312
|
end
|
@@ -1,22 +1,21 @@
|
|
1
|
-
|
2
1
|
module TrackingNumber
|
3
2
|
module Loader
|
4
3
|
class << self
|
5
|
-
def load_tracking_number_data(couriers_path = File.join(File.dirname(__FILE__),
|
4
|
+
def load_tracking_number_data(couriers_path = File.join(File.dirname(__FILE__), '../data/couriers/'))
|
6
5
|
mapping_data = []
|
7
6
|
tracking_number_types = []
|
8
7
|
Dir.glob(File.join(couriers_path, '/*.json')).each do |file|
|
9
|
-
courier_info
|
8
|
+
courier_info = read_courier_info(file)
|
10
9
|
|
11
10
|
courier_info[:tracking_numbers].each do |tracking_info|
|
12
|
-
|
13
|
-
klass = create_class(klass, courier_info, tracking_info)
|
11
|
+
klass = create_class(courier_info, tracking_info)
|
14
12
|
|
15
13
|
# Do some basic checks on the data file
|
16
14
|
throw 'missing test numbers' unless has_test_numbers?(tracking_info)
|
17
|
-
throw 'missing regex match groups' unless test_numbers_return_required_groups?(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
18
|
|
19
|
-
const = register_class(klass, tracking_name)
|
20
19
|
tracking_number_types.push(const)
|
21
20
|
mapping_data << {
|
22
21
|
class: const,
|
@@ -28,33 +27,33 @@ module TrackingNumber
|
|
28
27
|
end
|
29
28
|
|
30
29
|
TrackingNumber.const_set('TYPES', tracking_number_types)
|
31
|
-
|
32
|
-
|
30
|
+
|
31
|
+
mapping_data
|
33
32
|
end
|
34
33
|
|
35
34
|
private
|
36
35
|
|
37
36
|
def has_test_numbers?(tracking)
|
38
|
-
|
37
|
+
tracking[:test_numbers] && tracking[:test_numbers][:valid]
|
39
38
|
end
|
40
39
|
|
41
40
|
def test_numbers_return_required_groups?(tracking, regex)
|
42
41
|
test_number = tracking[:test_numbers][:valid][0]
|
43
42
|
matches = test_number.match(regex)
|
44
43
|
|
45
|
-
|
44
|
+
matches['SerialNumber']
|
46
45
|
end
|
47
46
|
|
48
47
|
def read_courier_info(file)
|
49
|
-
|
48
|
+
JSON.parse(File.read(file)).deep_symbolize_keys!
|
50
49
|
end
|
51
50
|
|
52
|
-
def create_class(
|
51
|
+
def create_class(courier_info, tracking_info)
|
53
52
|
klass = Class.new(TrackingNumber::Base)
|
54
|
-
klass.const_set(
|
53
|
+
klass.const_set('COURIER_CODE', courier_info[:courier_code])
|
55
54
|
info = courier_info.dup
|
56
55
|
info.delete(:tracking_numbers)
|
57
|
-
klass.const_set(
|
56
|
+
klass.const_set('COURIER_INFO', info)
|
58
57
|
|
59
58
|
pattern = tracking_info[:regex]
|
60
59
|
pattern = tracking_info[:regex].join if tracking_info[:regex].is_a?(Array)
|
@@ -62,19 +61,21 @@ module TrackingNumber
|
|
62
61
|
verify_pattern = "^#{pattern}$"
|
63
62
|
search_pattern = "\\b#{pattern}\\b"
|
64
63
|
|
65
|
-
klass.const_set(
|
66
|
-
klass.const_set(
|
64
|
+
klass.const_set('SEARCH_PATTERN', Regexp.new(search_pattern))
|
65
|
+
klass.const_set('VERIFY_PATTERN', Regexp.new(verify_pattern))
|
67
66
|
|
68
|
-
klass.const_set(
|
69
|
-
klass.const_set(
|
70
|
-
klass.const_set(
|
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])
|
71
72
|
|
72
|
-
|
73
|
+
klass
|
73
74
|
end
|
74
75
|
|
75
76
|
def register_class(klass, tracking_name)
|
76
77
|
klass_name = tracking_name.gsub(/[^0-9A-Za-z]/, '')
|
77
|
-
|
78
|
+
TrackingNumber.const_set(klass_name, klass)
|
78
79
|
end
|
79
80
|
end
|
80
81
|
end
|
data/lib/tracking_number.rb
CHANGED
@@ -5,6 +5,7 @@ require 'tracking_number/checksum_validations'
|
|
5
5
|
require 'tracking_number/loader'
|
6
6
|
require 'tracking_number/base'
|
7
7
|
require 'tracking_number/info'
|
8
|
+
require 'tracking_number/partnership'
|
8
9
|
require 'tracking_number/unknown'
|
9
10
|
require 'active_support/core_ext/string'
|
10
11
|
require 'active_support/core_ext/hash'
|
@@ -16,29 +17,51 @@ end
|
|
16
17
|
TrackingNumber::Loader.load_tracking_number_data
|
17
18
|
|
18
19
|
module TrackingNumber
|
19
|
-
def self.search(body)
|
20
|
-
TYPES.collect { |type| type.search(body) }.flatten
|
20
|
+
def self.search(body, match: :carrier)
|
21
|
+
matches = TYPES.collect { |type| type.search(body) }.flatten
|
22
|
+
|
23
|
+
# Some tracking numbers (e.g. Fedex Smartpost) are partnerships between two parties, where one party is the shipper (e.g. Fedex)
|
24
|
+
# and the other party is the [last mile] carrier (e.g. USPS). We're probably interested in the last mile aspect of
|
25
|
+
# the partnership, so by default we'll show those
|
26
|
+
|
27
|
+
# Tracking numbers without a partnership are both the shipper and carrier.
|
28
|
+
|
29
|
+
case match
|
30
|
+
when :carrier
|
31
|
+
matches.filter(&:carrier?)
|
32
|
+
when :shipper
|
33
|
+
matches.filter(&:shipper?)
|
34
|
+
when :all
|
35
|
+
matches
|
36
|
+
else
|
37
|
+
matches
|
38
|
+
end
|
21
39
|
end
|
22
40
|
|
23
|
-
def self.detect(tracking_number)
|
41
|
+
def self.detect(tracking_number, match: :carrier)
|
24
42
|
tn = nil
|
25
|
-
|
43
|
+
(TYPES + [Unknown]).each do |test_klass|
|
26
44
|
tn = test_klass.new(tracking_number)
|
27
|
-
|
45
|
+
if tn.valid? && (!match || match == :all)
|
46
|
+
break
|
47
|
+
elsif tn.valid? && tn.send("#{match}?")
|
48
|
+
break
|
49
|
+
end
|
28
50
|
end
|
29
|
-
|
51
|
+
tn
|
30
52
|
end
|
31
53
|
|
32
54
|
def self.detect_all(tracking_number)
|
33
55
|
matches = []
|
34
|
-
|
56
|
+
(TYPES + [Unknown]).each do |test_klass|
|
35
57
|
tn = test_klass.new(tracking_number)
|
36
58
|
matches << tn if tn.valid?
|
37
59
|
end
|
38
|
-
|
60
|
+
|
61
|
+
matches
|
39
62
|
end
|
40
63
|
|
41
64
|
def self.new(tracking_number)
|
42
|
-
|
65
|
+
detect(tracking_number)
|
43
66
|
end
|
44
67
|
end
|
data/test/test_helper.rb
CHANGED
@@ -18,8 +18,6 @@ class Minitest::Test
|
|
18
18
|
possible_numbers = []
|
19
19
|
possible_numbers << tracking
|
20
20
|
possible_numbers << tracking.to_s.gsub(" ", "")
|
21
|
-
possible_numbers << tracking.chars.to_a.join(" ")
|
22
|
-
possible_numbers << tracking.chars.to_a.join(" ")
|
23
21
|
possible_numbers << tracking.slice(0, (tracking.length / 2)) + " " + tracking.slice((tracking.length / 2), tracking.length)
|
24
22
|
|
25
23
|
possible_numbers.flatten.uniq
|
@@ -23,7 +23,7 @@ class TrackingNumberMetaTest < Minitest::Test
|
|
23
23
|
tracking_info[:test_numbers][:valid].each do |valid_number|
|
24
24
|
should "detect #{valid_number} as #{klass_name}" do
|
25
25
|
#TODO fix this multiple matching thing
|
26
|
-
matches = TrackingNumber.search(valid_number)
|
26
|
+
matches = TrackingNumber.search(valid_number, match: :all)
|
27
27
|
assert matches.collect(&:class).include?(klass)
|
28
28
|
end
|
29
29
|
|
@@ -147,9 +147,77 @@ class TrackingNumberTest < Minitest::Test
|
|
147
147
|
assert_nil tracking_number.package_type
|
148
148
|
end
|
149
149
|
|
150
|
+
should "report no partnership" do
|
151
|
+
assert_equal false, tracking_number.partnership?
|
152
|
+
end
|
153
|
+
|
154
|
+
should "report no partners" do
|
155
|
+
assert_equal nil, tracking_number.partners
|
156
|
+
end
|
157
|
+
|
158
|
+
should "report as shipper and carrier" do
|
159
|
+
assert_equal true, tracking_number.shipper?
|
160
|
+
assert_equal true, tracking_number.carrier?
|
161
|
+
end
|
162
|
+
|
150
163
|
should "have valid tracking url" do
|
151
164
|
assert tracking_number.tracking_url, "Tracking url should not be blank"
|
152
165
|
assert tracking_number.tracking_url.include?(tracking_number.tracking_number), "Should include tracking number in the url"
|
153
166
|
end
|
154
167
|
end
|
168
|
+
|
169
|
+
context "tracking number partnership data for FedExSmartPost/USPS91" do
|
170
|
+
tracking_number = TrackingNumber.new("420 11213 92 6129098349792366623 8")
|
171
|
+
|
172
|
+
should "report correct courier name" do
|
173
|
+
assert_equal "United States Postal Service", tracking_number.courier_name
|
174
|
+
end
|
175
|
+
|
176
|
+
should "report correct courier code" do
|
177
|
+
assert_equal :usps, tracking_number.courier_code
|
178
|
+
end
|
179
|
+
|
180
|
+
should "report correct service type" do
|
181
|
+
assert_equal "Fedex Smart Post", tracking_number.service_type
|
182
|
+
end
|
183
|
+
|
184
|
+
should "report partnership" do
|
185
|
+
assert_equal true, tracking_number.partnership?
|
186
|
+
end
|
187
|
+
|
188
|
+
should "report not shipper side of the partnership" do
|
189
|
+
assert_equal false, tracking_number.shipper?
|
190
|
+
end
|
191
|
+
|
192
|
+
should "report carrier side of the partnership" do
|
193
|
+
assert_equal true, tracking_number.carrier?
|
194
|
+
end
|
195
|
+
|
196
|
+
should "report partner pairing" do
|
197
|
+
assert_equal :fedex, tracking_number.partners.shipper.courier_code
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context "searching numbers that have partners" do
|
202
|
+
partnership_number = "420 11213 92 6129098349792366623 8"
|
203
|
+
single_number = "0307 1790 0005 2348 3741"
|
204
|
+
|
205
|
+
search_string = ["number that matches two services", partnership_number, " number that matches only one: ", single_number, "let's see if that does it"].join(' ')
|
206
|
+
|
207
|
+
should "match only carriers by default" do
|
208
|
+
matches = TrackingNumber.search(search_string)
|
209
|
+
assert_equal 2, matches.size
|
210
|
+
assert_equal [true, true], matches.collect { |t| t.carrier? }
|
211
|
+
end
|
212
|
+
|
213
|
+
should "match all if specified" do
|
214
|
+
matches = TrackingNumber.search(search_string, match: :all)
|
215
|
+
assert_equal 3, matches.size
|
216
|
+
end
|
217
|
+
|
218
|
+
should "match only shippers if specified" do
|
219
|
+
matches = TrackingNumber.search(search_string, match: :shipper)
|
220
|
+
assert_equal 2, matches.size
|
221
|
+
end
|
222
|
+
end
|
155
223
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tracking_number
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Keen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -140,7 +140,8 @@ description: This gem identifies valid tracking numbers and the service they're
|
|
140
140
|
with. It can also tell you a little bit about the package purely from the number—there's
|
141
141
|
quite a bit of info tucked away into those numbers, it turns out.
|
142
142
|
email: jeff@keen.me
|
143
|
-
executables:
|
143
|
+
executables:
|
144
|
+
- console
|
144
145
|
extensions: []
|
145
146
|
extra_rdoc_files:
|
146
147
|
- LICENSE.txt
|
@@ -157,6 +158,7 @@ files:
|
|
157
158
|
- LICENSE.txt
|
158
159
|
- README.md
|
159
160
|
- Rakefile
|
161
|
+
- bin/console
|
160
162
|
- lib/data/couriers/amazon.json
|
161
163
|
- lib/data/couriers/canadapost.json
|
162
164
|
- lib/data/couriers/dhl.json
|
@@ -174,6 +176,7 @@ files:
|
|
174
176
|
- lib/tracking_number/checksum_validations.rb
|
175
177
|
- lib/tracking_number/info.rb
|
176
178
|
- lib/tracking_number/loader.rb
|
179
|
+
- lib/tracking_number/partnership.rb
|
177
180
|
- lib/tracking_number/unknown.rb
|
178
181
|
- lib/tracking_number/version.rb
|
179
182
|
- package.json
|