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 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