tracking_number 1.3.5 → 1.4.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/.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
|