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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8a86aeaa8c06073cb379d6c64b1637b8246c8cd54f821696c20c39348061b2
4
- data.tar.gz: 8e561f7d1ea8c26d0003297ea4ce5cf9835b805bb6278714c10f81a164375f38
3
+ metadata.gz: 632e88e03ffd1404e26689c874e6093d8b978ceb7326d0e9c13879ffcf5c4a27
4
+ data.tar.gz: eb6acde6415fb26c3e303ba155714b8f033a14108b4b39a123f841b38d0310ba
5
5
  SHA512:
6
- metadata.gz: 5920313b3ea08d709c564a3ca5b03e9ee34e0fe213e83fbdee53680504a24f02dac5fa9e774f864d77259e960100177abf9430b5e717991ec589d881f9f9d246
7
- data.tar.gz: df3aab7b35d0cbfaa9b012439de32f0e38dac1217d65c14939dccbc8792ccb918059f58eafcb2f37d52de9f0b911ccaac8bfd16dceb9f0b94923fa0de8a94695
6
+ metadata.gz: 5816c80bcd06b12d4918ca9b8b017035eb9f8caa2fb79c47a457e92863636d2263aa2f09abe757fe5219d149eb940a67556f4efcfa43501cdbb47ef304ee743a
7
+ data.tar.gz: 416f4a346ad8449e39d0bb570fbbd9ea157c19062f32d7ffc4cc8f2514a5e75b04ee8fa4715ac6a1fc2667de4f8264f8763b43a249a88357a2c50b1eb99c6333
@@ -7,7 +7,7 @@ on:
7
7
  jobs:
8
8
  release:
9
9
  name: Release
10
- runs-on: ubuntu-18.04
10
+ runs-on: ubuntu-latest
11
11
  steps:
12
12
  - name: Checkout
13
13
  uses: actions/checkout@v3
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
- " T B A 0 0 0 0 0 0 0 0 0 0 0 0 ",
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
  ],
@@ -4,6 +4,7 @@
4
4
  "tracking_numbers": [
5
5
  {
6
6
  "name": "Canada Post (16)",
7
+ "id": "canada_post",
7
8
  "regex": "\\s*(?<SerialNumber>(?<OriginId>([0-9]\\s*){7})([0-9]\\s*){8})(?<CheckDigit>[0-9]\\s*)",
8
9
  "validation": {
9
10
  "checksum": {
@@ -4,7 +4,8 @@
4
4
  "tracking_numbers": [
5
5
  {
6
6
  "name": "DHL Express",
7
- "regex": "\\s*(?<SerialNumber>([0-9]\\s*){9})(?<CheckDigit>([0-9]\\s*))",
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
- "regex": "\\s*((G\\s*M\\s*)|(L\\s*X\\s*)|(R\\s*X\\s*)|(U\\s*V\\s*)|(C\\s*N\\s*)|(S\\s*G\\s*)|(T\\s*H\\s*)|(I\\s*N\\s*)|(H\\s*K\\s*)|(M\\s*Y\\s*))\\s*(?<SerialNumber>([0-9]\\s*){10,39})",
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": [
@@ -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
- "description": "IMpb CO3 standard",
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
- "(?<ServiceType>([0-9]\\s*){3})",
99
- "(?<ShipperId>([0-9]\\s*){9})",
100
- "(?<PackageId>([0-9]\\s*){7})",
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": "Landmark Global LTN",
7
+ "id": "landmark_global",
7
8
  "regex": [
8
9
  "\\s*L\\s*T\\s*N\\s*(?<SerialNumber>([0-9]\\s*){8})\\s*N\\s*1"
9
10
  ],
@@ -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
- " C 1 1 0 3 1 5 0 0 0 0 1 8 7 9 ",
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": [
@@ -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",
@@ -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>([A-Z]\\s*){1})",
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",
@@ -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
  }
@@ -1,15 +1,16 @@
1
1
  module TrackingNumber
2
2
  class Base
3
- attr_accessor :tracking_number
4
- attr_accessor :original_number
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(" ", "").upcase
9
+ @tracking_number = tracking_number.strip.gsub(' ', '').upcase
9
10
  end
10
11
 
11
12
  def self.search(body)
12
- valids = self.scan(body).uniq.collect { |possible| new(possible) }.select { |t| t.valid? }
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(self.const_get(:SEARCH_PATTERN)){
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("SerialNumber") unless self.class.const_get("VALIDATION")
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("SerialNumber")
43
+ raw_serial = match_group('SerialNumber')
43
44
 
44
- if format_info
45
- if 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[: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
- return raw_serial
51
+ raw_serial
53
52
  end
54
53
 
55
54
  def check_digit
56
- match_group("CheckDigit")
55
+ match_group('CheckDigit')
57
56
  end
58
57
 
59
58
  def decode
60
59
  decoded = {}
61
- (self.matches.try(:names) || []).each do |name|
60
+ (matches.try(:names) || []).each do |name|
62
61
  sym = name.underscore.to_sym
63
- decoded[sym] = self.matches[name]
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
- return true
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("VALIDATION")[:additional]
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 self.valid_format?
90
- checksum_info = self.class.const_get(:VALIDATION)[:checksum]
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
- self.tracking_number
101
+ tracking_number
101
102
  end
102
103
 
103
104
  def inspect
104
- "#<%s:%#0x %s>" % [self.class.to_s, self.object_id, tracking_number]
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
- :courier => courier_info,
110
- :service_type => service_type,
111
- :service_description => service_description,
112
- :destination_zip => destination_zip,
113
- :shipper_id => shipper_id,
114
- :package_type => package_type
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["Courier"]
124
- matching_additional["Courier"][:courier]
125
- else
126
- if self.class.constants.include?(:COURIER_INFO)
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
- alias_method :carrier, :courier_code #OG tracking_number gem used :carrier.
133
- alias_method :carrier_code, :courier_code
134
- alias_method :carrier_name, :courier_name
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 = {:name => courier_name, :code => courier_code}
139
+ basics = { name: courier_name, code: courier_code }
138
140
 
139
- if info = matching_additional["Courier"]
140
- basics.merge!(:name => info[:courier], :url => info[:courier_url], :country => info[:country])
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 service_type
147
- if matching_additional["Service Type"]
148
- @service_type ||= Info.new(matching_additional["Service Type"]).name
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["Service Type"]
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["Container Type"]
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("DestinationZip")
193
+ match_group('DestinationZip')
166
194
  end
167
195
 
168
196
  def shipper_id
169
- match_group("ShipperId")
197
+ match_group('ShipperId')
170
198
  end
171
199
 
172
200
  def tracking_url
173
201
  url = nil
174
- if matching_additional["Courier"]
175
- url = matching_additional["Courier"][:tracking_url]
176
- else
177
- if self.class.const_defined?(:TRACKING_URL)
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 |info|
193
- if self.matches && self.matches.length > 0
194
- if value = self.matches[info[:regex_group_name]].gsub(/\s/, "")
195
- # has matching value
196
- matches = info[:lookup].find do |i|
197
- if i[:matches]
198
- value == i[:matches]
199
- elsif i[:matches_regex]
200
- value =~ Regexp.new(i[:matches_regex])
201
- end
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 matches
215
- if self.class.constants.include?(:VERIFY_PATTERN)
216
- self.tracking_number.match(self.class.const_get(:VERIFY_PATTERN))
217
- else
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
- begin
224
- self.matches[name].gsub(/\s/, '')
225
- rescue
226
- nil
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__), "../data/couriers/"))
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 = read_courier_info(file)
8
+ courier_info = read_courier_info(file)
10
9
 
11
10
  courier_info[:tracking_numbers].each do |tracking_info|
12
- tracking_name = tracking_info[:name]
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, Regexp.new(klass::VERIFY_PATTERN))
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
- return mapping_data
30
+
31
+ mapping_data
33
32
  end
34
33
 
35
34
  private
36
35
 
37
36
  def has_test_numbers?(tracking)
38
- return tracking[:test_numbers] && tracking[:test_numbers][:valid]
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
- return matches["SerialNumber"]
44
+ matches['SerialNumber']
46
45
  end
47
46
 
48
47
  def read_courier_info(file)
49
- return JSON.parse(File.read(file)).deep_symbolize_keys!
48
+ JSON.parse(File.read(file)).deep_symbolize_keys!
50
49
  end
51
50
 
52
- def create_class(klass, courier_info, tracking_info)
51
+ def create_class(courier_info, tracking_info)
53
52
  klass = Class.new(TrackingNumber::Base)
54
- klass.const_set("COURIER_CODE", courier_info[:courier_code])
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("COURIER_INFO", info)
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("SEARCH_PATTERN", Regexp.new(search_pattern))
66
- klass.const_set("VERIFY_PATTERN", Regexp.new(verify_pattern))
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("VALIDATION", tracking_info[:validation])
69
- klass.const_set("ADDITIONAL", tracking_info[:additional])
70
- klass.const_set("TRACKING_URL", tracking_info[:tracking_url])
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
- return klass
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
- return TrackingNumber.const_set(klass_name, klass)
78
+ TrackingNumber.const_set(klass_name, klass)
78
79
  end
79
80
  end
80
81
  end
@@ -0,0 +1,7 @@
1
+ module TrackingNumber
2
+ class Partnership
3
+ def initialize(tracking_numbers)
4
+
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module TrackingNumber
2
- VERSION = "1.3.5"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -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
- for test_klass in (TYPES+[Unknown])
43
+ (TYPES + [Unknown]).each do |test_klass|
26
44
  tn = test_klass.new(tracking_number)
27
- break if tn.valid?
45
+ if tn.valid? && (!match || match == :all)
46
+ break
47
+ elsif tn.valid? && tn.send("#{match}?")
48
+ break
49
+ end
28
50
  end
29
- return tn
51
+ tn
30
52
  end
31
53
 
32
54
  def self.detect_all(tracking_number)
33
55
  matches = []
34
- for test_klass in (TYPES+[Unknown])
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
- return matches
60
+
61
+ matches
39
62
  end
40
63
 
41
64
  def self.new(tracking_number)
42
- self.detect(tracking_number)
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.3.5
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-01-09 00:00:00.000000000 Z
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