tracking_number 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75fcefe87240b40ddf1bd16c155f13f93f46b9cbe96b343fb9b480875dd82ef6
4
- data.tar.gz: f7ff78fe0acdc1a65ae6ddc63fe6e975ee4481c05e8d934b5fc8f09b1d2990ae
3
+ metadata.gz: 71f01e80c118f12c210f3c9a5195edf0c9d257c37ff4ce36529962f4131a57ed
4
+ data.tar.gz: 79519ddef14f7728eb6de46437360a9040368e261ce3dba4cdadf9a9b382891e
5
5
  SHA512:
6
- metadata.gz: e4d70982f20080077fff3be398bc0f99baabd1d67c8a579e2253735a532dbfe0d1bc38edb91df9e28f873ce1683ba26177a447c26d0503fa1c5fc2047f156a0e
7
- data.tar.gz: 1b22b16347f3ed186f7f539dc4bfc1a32ff07719faaf1cebc5a0983ced2139db4436303ab3405d4ad9b62081bdd0b632e769dc3822a1f7a5d47709e2832ca8f1
6
+ metadata.gz: 86ef4bdd51d50a76f26b0bc53f17db5518c48973ecc715b90058dc446c1a7ac2558db35a75f9a572837e4aacbf9e6fa33b2c5870dfc21076a50a8c538e156313
7
+ data.tar.gz: f78a9a8b106ce172f87f231c9d59aa6c52a1f76b8a6b471aaa5a17ac92ee605d71cab5d216b3032d3eba8fd0244a406f6358f6be465e1332c1edce32d07c9ad8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  tracking_number changelog
2
2
 
3
+ # [3.0.0](https://github.com/jkeen/tracking_number/compare/v2.5.0...v3.0.0) (2026-08-15)
4
+
5
+
6
+ * feat!: Update to tracking_number_data 2.0, which rebuilds the IMpb USPS definitions from Publication 199 and absorbs the DHL and Fedex definitions that described the same tracking number. ([448e953](https://github.com/jkeen/tracking_number/commit/448e9534b0be8d2f205de5712e3215281922de99))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * `fedex_smartpost`, `dhl_ecommerce_30` and `usps_32V2` are absorbed into `usps_impb_c`, which is the definition they were always describing. `usps_91` and `usps_22` become `usps_impb_c` and `usps_impb_n`, and Application Identifier 91 splits off to `usps_legacy`. Every number these matched still decodes, now as USPS.
12
+
13
+ No active definitions declare `partners` anymore, but it remains valid in our data spec for potential future use.
14
+
3
15
  # [2.5.0](https://github.com/jkeen/tracking_number/compare/v2.4.0...v2.5.0) (2026-08-13)
4
16
 
5
17
 
data/README.md CHANGED
@@ -128,32 +128,6 @@ Most tracking numbers have a format where each part of the number has meaning. `
128
128
  # }
129
129
  ```
130
130
 
131
- #### Multiple shippers / Partnerships
132
- Some tracking numbers match multiple carriers, because they belong to multiple carriers. Some shipments like Fedex Smartpost contract the "last mile" out to USPS.
133
-
134
- ```ruby
135
- # Search defaults to only showing numbers that fulfill the carrier side of the relationship
136
- # (if a partnership exists at all), as this is the end a consumer would most likely be interested in.
137
-
138
- results = TrackingNumber.search('420112139261290983497923666238')
139
- => [#<TrackingNumber::USPS91:0x26ac0 420112139261290983497923666238>]
140
-
141
- all_results = TrackingNumber.search('420112139261290983497923666238', match: :all)
142
- => [#<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>, #<TrackingNumber::USPS91:0x26ac0 420112139261290983497923666238>]
143
-
144
- tn = results.first
145
- tn.shipper? #=> false
146
- tn.carrier? #=> true
147
- tn.partnership? #=> true
148
- tn.partners
149
- #=> <struct TrackingNumber::Base::PartnerStruct
150
- # shipper=#<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>,
151
- # carrier=#<TrackingNumber::USPS91:0x2f1fc 420112139261290983497923666238>>
152
-
153
- tn.partners.shipper #=> #<TrackingNumber::FedExSmartPost:0x30624 420112139261290983497923666238>
154
- tn.partners.carrier == tn #=> true
155
- ```
156
-
157
131
  ## ActiveModel validation
158
132
 
159
133
  For Rails 3 (or any ActiveModel client), validate your fields as a tracking number:
@@ -90,81 +90,8 @@
90
90
  "GR295117494011169045"
91
91
  ]
92
92
  }
93
- },
94
- {
95
- "name": "DHL E-Commerce (30)",
96
- "id": "dhl_ecommerce_30",
97
- "regex": [
98
- "\\s*(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})",
99
- "(?!\\s*9\\s*[1-5]\\b)",
100
- "(?<SerialNumber>",
101
- "(?<ServiceType>(9[2-5]\\s*))",
102
- "(?<ShipperId>([0-9]\\s*){9})",
103
- "(?<PackageId>([0-9]\\s*){10})",
104
- ")",
105
- "(?<CheckDigit>[0-9]\\s*)"
106
- ],
107
- "validation": {
108
- "checksum": {
109
- "name": "mod10",
110
- "modulo": 10,
111
- "evens_multiplier": 3,
112
- "odds_multiplier": 1,
113
- "reverse": true
114
- }
115
- },
116
- "tracking_url": "https://ecommerceportal.dhl.com/track?trackingnumber=%s",
117
- "partners": [{
118
- "description": "DHL eCommerce hands off to USPS for the last mile",
119
- "partner_type": "carrier",
120
- "partner_id": "usps_22",
121
- "validation": {
122
- "matches_all": [
123
- { "regex_group_name": "RoutingApplicationId", "matches": "420" },
124
- { "regex_group_name": "ServiceType", "matches_regex": "9[2345]" }
125
- ]
126
- }
127
- },
128
- {
129
- "description": "DHL eCommerce hands off to USPS for the last mile",
130
- "partner_type": "carrier",
131
- "partner_id": "usps_91",
132
- "validation": {
133
- "matches_all": [
134
- { "regex_group_name": "RoutingApplicationId", "matches": "420" },
135
- { "regex_group_name": "ServiceType", "matches_regex": "9[2345]" }
136
- ]
137
- }
138
- }],
139
- "additional": [
140
- {
141
- "name": "Service Type",
142
- "regex_group_name": "ServiceType",
143
- "lookup": [
144
- { "matches": "92", "name": "USPS Tracking (Commercial)" },
145
- { "matches": "93", "name": "Collect on Delivery (COD)" },
146
- { "matches": "94", "name": "USPS Tracking" },
147
- { "matches": "95", "name": "Certified Mail" }
148
- ]
149
- }
150
- ],
151
- "test_numbers": {
152
- "valid": [
153
- "420902459261290336128704042634",
154
- "420941179261290336128704062441",
155
- "420926029261290336128704067248",
156
- "420902729261290336128704124163",
157
- "420950549261290336128704133837",
158
- "420193809261290336128704280838",
159
- "420900469261290336128704216936"
160
- ],
161
- "invalid": [
162
- "420941179261290336128704062442",
163
- "420941179261290336128704062400",
164
- "420941179261290336128704062422"
165
- ]
166
- }
167
93
  }
168
94
 
95
+
169
96
  ]
170
97
  }
@@ -92,71 +92,37 @@
92
92
  }
93
93
  },
94
94
  {
95
- "name": "FedEx SmartPost",
96
- "id": "fedex_smartpost",
97
- "description": "Shipped by FedEx, Delivered by USPS",
98
- "glossary": {
99
- "ApplicationIdentifier": { "description": "Always 92 here, the code for a parcel handed to USPS for delivery." }
100
- },
95
+ "name": "FedEx ASTRA (32)",
96
+ "id": "fedex_astra_32",
97
+ "description": "The legacy ASTRA barcode. Positions 17 through 28 hold a FedEx Express tracking number, and its check digit is the only one the barcode carries. What the other fields mean is not yet known.",
101
98
  "regex": [
102
- "\\s*(?:",
103
- "(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
104
- "(?<ApplicationIdentifier>9\\s*2\\s*)",
105
- ")?",
106
- "(?<SerialNumber>",
107
- "(?<SCNC>6\\s*1\\s*)",
108
- "(?<ServiceType>2\\s*9\\s*)",
109
- "(?<ShipperId>([0-9]\\s*){8})",
110
- "(?<PackageId>([0-9]\\s*){11}|([0-9]\\s*){7})",
111
- ")",
112
- "(?<CheckDigit>([0-9]\\s*))"
113
- ],
114
- "additional": [
115
- {
116
- "name": "Service Type",
117
- "regex_group_name": "ServiceType",
118
- "lookup": [
119
- {
120
- "matches_regex": ".",
121
- "name": "Delivered by USPS"
122
- }
123
- ]
124
- }
99
+ "\\s*3\\s*",
100
+ "(?:[0-9]\\s*){5}",
101
+ "(?:[0-9]\\s*){3}",
102
+ "(?:[0-9]\\s*){7}",
103
+ "(?<SerialNumber>(?:[0-9]\\s*){11})",
104
+ "(?<CheckDigit>[0-9]\\s*)",
105
+ "(?:[0-9]\\s*){4}"
125
106
  ],
126
- "partners": [{
127
- "partner_id": "usps_91",
128
- "partner_type": "carrier",
129
- "description": "FedEx SmartPost is a shipping service that utilizes FedEx for the initial transport and the United States Postal Service for final delivery."
130
- }],
131
107
  "validation": {
132
108
  "checksum": {
133
- "name": "mod10",
134
- "modulo": 10,
135
- "evens_multiplier": 3,
136
- "odds_multiplier": 1,
137
- "reverse": false
138
- },
139
- "serial_number_format": {
140
- "prepend_if": {
141
- "matches_regex": "^(?!92).+",
142
- "content": "92"
143
- }
109
+ "name": "sum_product_with_weightings_and_modulo",
110
+ "weightings": [3, 1, 7, 3, 1, 7, 3, 1, 7, 3, 1],
111
+ "modulo1": 11,
112
+ "modulo2": 10
144
113
  }
145
114
  },
146
- "tracking_url": "https://www.fedex.com/apps/fedextrack/?tracknumbers=%s",
147
115
  "test_numbers": {
148
116
  "valid": [
149
- "9261292700768711948021",
150
- "420 11213 92 6129098349792366623 8",
151
- "92 6129098349792366623 8"
117
+ "32971514560102447849175802862014",
118
+ "32971510360102447848540980802018",
119
+ "32971508360102447847941133172013",
120
+ "33810799560000004380417923803212",
121
+ "3 29715 145 6010244 784917580286 2014"
152
122
  ],
153
123
  "invalid": [
154
- "9261292700768711948020",
155
- "92001903060085300042901077",
156
- "420 11213 61290983497923666231",
157
- "92 6129098349792366623 5",
158
- "9400 1112 0108 0805 4830 16",
159
- "9361 2898 7870 0317 6337 95"
124
+ "32971514560102447849175802852014",
125
+ "3297151456010244784917580286201"
160
126
  ]
161
127
  }
162
128
  },
@@ -238,8 +204,7 @@
238
204
  "name": "FedEx Ground 96 (22)",
239
205
  "id": "fedex_ground_96",
240
206
  "glossary": {
241
- "ApplicationIdentifier": { "description": "Always 96, marking a ground shipping label." },
242
- "SCNC": { "description": "FedEx Ground uses 11, 12 and 13." }
207
+ "ApplicationIdentifier": { "description": "Always 96, marking a ground shipping label." }
243
208
  },
244
209
  "regex": [
245
210
  "\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
@@ -271,9 +236,10 @@
271
236
  {
272
237
  "name": "FedEx Ground GSN",
273
238
  "id": "fedex_ground_gsn",
239
+ "description": "Ground's 34 digit barcode, carrying the Ground shipper number and the tracking number in the last twelve digits",
274
240
  "glossary": {
275
241
  "ApplicationIdentifier": { "description": "Always 96, marking a ground shipping label." },
276
- "SCNC": { "description": "FedEx Ground uses 11, 12 and 13." }
242
+ "SCNC": { "description": "22, or 32 where the shipper number is nine digits." }
277
243
  },
278
244
  "regex": [
279
245
  "\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
@@ -4,6 +4,7 @@
4
4
  "tracking_numbers": [
5
5
  {
6
6
  "id": "s10",
7
+ "description": "The UPU format every postal service uses for international items",
7
8
  "name": "S10",
8
9
  "validation": {
9
10
  "checksum": {
@@ -5,7 +5,7 @@
5
5
  {
6
6
  "name": "USPS 20",
7
7
  "id": "usps_20",
8
- "description": "20 digit USPS numbers",
8
+ "description": "Delivery and Signature Confirmation numbers, printed without the application identifier",
9
9
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
10
10
  "regex": [
11
11
  "\\s*(?<SerialNumber>",
@@ -39,37 +39,47 @@
39
39
  "name": "Service Type",
40
40
  "regex_group_name": "ServiceType",
41
41
  "lookup": [
42
- {
43
- "matches": "71",
44
- "name": "Certified Mail"
45
- },
46
- {
47
- "matches": "73",
48
- "name": "Insured Mail"
49
- },
50
- {
51
- "matches": "77",
52
- "name": "Registered Mail"
53
- },
54
- {
55
- "matches": "81",
56
- "name": "Return Receipt For Merchanise"
57
- }
42
+ { "matches": "01", "name": "Delivery Confirmation", "description": "Priority Mail or First-Class Mail Parcels" },
43
+ { "matches": "02", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels" },
44
+ { "matches_regex": "0[34]", "name": "Delivery Confirmation", "description": "Retail" },
45
+ { "matches": "05", "name": "Delivery Confirmation", "description": "Priority Mail or First-Class Mail Parcels, insured over $50" },
46
+ { "matches": "06", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured over $50" },
47
+ { "matches": "07", "name": "Delivery Confirmation", "description": "Priority Mail, insured $50 or under" },
48
+ { "matches": "08", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured $50 or under" },
49
+ { "matches": "14", "name": "Delivery Confirmation", "description": "Software vendors only, by approval" },
50
+ { "matches": "21", "name": "Signature Confirmation", "description": "Priority Mail or First-Class Mail Parcels" },
51
+ { "matches": "22", "name": "Signature Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels" },
52
+ { "matches_regex": "2[34]", "name": "Signature Confirmation", "description": "Retail" },
53
+ { "matches": "25", "name": "Signature Confirmation", "description": "Priority Mail or First-Class Mail Parcels, insured, all values" },
54
+ { "matches": "26", "name": "Signature Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured, all values" },
55
+ { "matches": "34", "name": "Signature Confirmation", "description": "Software vendors only, by approval" },
56
+ { "matches": "50", "name": "Electronic File Header", "description": "Used in the file header record only, never on a package" },
57
+ { "matches": "56", "name": "Passive Enroute Scan", "description": "A free scan as the parcel passes through a sorting plant, with no confirmation service attached" },
58
+ { "matches": "73", "name": "Insured Mail", "description": "Insured only, value over $50" },
59
+ { "matches": "82", "name": "Merchandise Return", "description": "With Delivery Confirmation" },
60
+ { "matches": "83", "name": "Merchandise Return", "description": "With Delivery Confirmation, insured value $50 or under" },
61
+ { "matches": "85", "name": "Merchandise Return", "description": "With Delivery Confirmation, insured value over $50" }
58
62
  ]
59
63
  }
60
64
  ]
61
65
  },
62
66
  {
63
- "name": "USPS 22",
64
- "id": "usps_22",
65
- "description": "22 digit USPS numbers",
67
+ "name": "USPS IMpb N",
68
+ "id": "usps_impb_n",
69
+ "description": "IMpb constructs N01 through N10, issued online or by a vendor or meter",
70
+ "glossary": {
71
+ "ShipperId": { "description": "The company that holds the postal permit the package was mailed under, which Publication 199 calls the Mailer ID. Nine digits when it starts with 9, six otherwise." }
72
+ },
66
73
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
67
74
  "regex": [
68
- "\\s*((?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
75
+ "\\s*((?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}(?=([0-9]\\s*){22}(?![0-9])|([0-9]\\s*){26}(?![0-9])))(?<DestinationZipPlus4>([0-9]\\s*){4}(?=([0-9]\\s*){22}(?![0-9])))?)?",
69
76
  "(?<SerialNumber>",
70
- "(?<ServiceType>(9\\s*[4-8]\\s*))",
71
- "(?<ShipperId>([0-9]\\s*){9})",
72
- "(?<PackageId>([0-9]\\s*){10})",
77
+ "(?<ApplicationIdentifier>9\\s*4\\s*)",
78
+ "(?<ServiceType>([0-9]\\s*){3})",
79
+ "(?=9\\s*([0-9]\\s*){8}(([0-9]\\s*){15}|([0-9]\\s*){11}|([0-9]\\s*){7})[0-9]\\s*(?![0-9])",
80
+ "|[0-8]\\s*([0-9]\\s*){5}(([0-9]\\s*){14}|([0-9]\\s*){10})[0-9]\\s*(?![0-9]))",
81
+ "(?<ShipperId>9\\s*([0-9]\\s*){8}|[0-8]\\s*([0-9]\\s*){5})",
82
+ "(?<PackageId>([0-9]\\s*){15}|([0-9]\\s*){14}|([0-9]\\s*){11}|([0-9]\\s*){10}|([0-9]\\s*){7})",
73
83
  ")",
74
84
  "(?<CheckDigit>[0-9]\\s*)"
75
85
  ],
@@ -86,6 +96,8 @@
86
96
  "valid": [
87
97
  "420787459400111206206406260787",
88
98
  "9400111206206406260787",
99
+ "9400 1112 0108 0805 4830 16",
100
+ "9405803699300124287899",
89
101
  "9434611206206406227577",
90
102
  "9434611206206407667136",
91
103
  "9400111206206407628746",
@@ -97,53 +109,30 @@
97
109
  "2334611306206407667222"
98
110
  ]
99
111
  },
100
- "partners": [{
101
- "description": "DHL ECommerce uses USPS for last mile delivery",
102
- "partner_type": "shipper",
103
- "partner_id": "dhl_ecommerce_30",
104
- "validation": {
105
- "matches_all": [
106
- {
107
- "regex_group_name": "RoutingApplicationId",
108
- "matches": "420"
109
- },
110
- {
111
- "matches_regex": "9[24]",
112
- "regex_group_name": "ServiceType"
113
- }
114
- ]
115
- }
116
- }],
117
112
  "additional": [
118
113
  {
119
- "name": "Service Type",
120
- "regex_group_name": "ServiceType",
114
+ "name": "Application Identifier",
115
+ "regex_group_name": "ApplicationIdentifier",
121
116
  "lookup": [
122
- { "matches": "92", "name": "Signature Confirmation" },
123
- { "matches": "93", "name": "Collect on Delivery (COD)" },
124
- { "matches": "94", "name": "USPS Tracking" },
125
- { "matches": "95", "name": "Certified Mail" },
126
- { "matches": "96", "name": "Insured Mail" },
127
- { "matches": "97", "name": "Registered Mail" },
128
- { "matches": "98", "name": "Return Receipt for Merchandise" }
117
+ { "matches": "94", "name": "Online or vendor label", "description": "Issued through USPS WebTools, or an authorized PC Postage provider" }
129
118
  ]
130
119
  }
131
120
  ]
132
121
  },
133
122
  {
134
- "name": "USPS 34v2",
135
- "id": "usps_32V2",
136
- "description": "variation on 34 digit USPS IMpd numbers",
123
+ "name": "USPS Legacy",
124
+ "id": "usps_legacy",
125
+ "description": "Delivery and Signature Confirmation numbers, carrying the 91 application identifier",
137
126
  "glossary": {
138
- "ApplicationIdentifier": { "description": "92 through 95, saying which USPS service and mailer ID scheme the rest belongs to." }
127
+ "ApplicationIdentifier": { "description": "Present only when the barcode is UCC/EAN Code 128." }
139
128
  },
140
129
  "regex": [
141
- "\\s*(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})",
142
- "(?<RoutingNumber>([0-9]\\s*){4})",
130
+ "\\s*(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})(?<DestinationZipPlus4>([0-9]\\s*){4})?)?",
143
131
  "(?<SerialNumber>",
144
- "(?<ApplicationIdentifier>9\\s*[2345]\\s*)?",
145
- "(?<ShipperId>([0-9]\\s*){8})",
146
- "(?<PackageId>([0-9]\\s*){11})",
132
+ "(?<ApplicationIdentifier>9\\s*1\\s*)?",
133
+ "(?<ServiceType>([0-9]\\s*){2})",
134
+ "(?<ShipperId>([0-9]\\s*){9})",
135
+ "(?<PackageId>([0-9]\\s*){8})",
147
136
  ")",
148
137
  "(?<CheckDigit>[0-9]\\s*)"
149
138
  ],
@@ -154,35 +143,71 @@
154
143
  "evens_multiplier": 3,
155
144
  "odds_multiplier": 1,
156
145
  "reverse": false
146
+ },
147
+ "serial_number_format": {
148
+ "prepend_if": {
149
+ "matches_regex": "^(?!9\\s*1).+",
150
+ "content": "91"
151
+ }
157
152
  }
158
153
  },
159
154
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
160
155
  "test_numbers": {
161
156
  "valid": [
162
- "4201002334249200190132607600833457",
163
- "4201028200009261290113185417468510",
164
- " 4 2 0 1 0 2 8 2 0 0 0 0 9 2 6 1 2 9 0 1 1 3 1 8 5 4 1 7 4 6 8 5 1 0 "
157
+ "420 22153 9101026837331000039521",
158
+ "9101 1234 5678 9000 0000 13",
159
+ "7196 9010 7560 0307 7385"
165
160
  ],
166
161
  "invalid": [
167
- "4201028200009261290113185417468511"
162
+ "9101 1234 5678 9000 0000 14",
163
+ "7196 9010 7560 0307 7386"
168
164
  ]
169
- }
165
+ },
166
+ "additional": [
167
+ {
168
+ "name": "Service Type",
169
+ "regex_group_name": "ServiceType",
170
+ "lookup": [
171
+ { "matches": "01", "name": "Delivery Confirmation", "description": "Priority Mail or First-Class Mail Parcels" },
172
+ { "matches": "02", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels" },
173
+ { "matches_regex": "0[34]", "name": "Delivery Confirmation", "description": "Retail" },
174
+ { "matches": "05", "name": "Delivery Confirmation", "description": "Priority Mail or First-Class Mail Parcels, insured over $50" },
175
+ { "matches": "06", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured over $50" },
176
+ { "matches": "07", "name": "Delivery Confirmation", "description": "Priority Mail, insured $50 or under" },
177
+ { "matches": "08", "name": "Delivery Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured $50 or under" },
178
+ { "matches": "14", "name": "Delivery Confirmation", "description": "Software vendors only, by approval" },
179
+ { "matches": "21", "name": "Signature Confirmation", "description": "Priority Mail or First-Class Mail Parcels" },
180
+ { "matches": "22", "name": "Signature Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels" },
181
+ { "matches_regex": "2[34]", "name": "Signature Confirmation", "description": "Retail" },
182
+ { "matches": "25", "name": "Signature Confirmation", "description": "Priority Mail or First-Class Mail Parcels, insured, all values" },
183
+ { "matches": "26", "name": "Signature Confirmation", "description": "Package Services, Parcel Select or Standard Mail Parcels, insured, all values" },
184
+ { "matches": "34", "name": "Signature Confirmation", "description": "Software vendors only, by approval" },
185
+ { "matches": "50", "name": "Electronic File Header", "description": "Used in the file header record only, never on a package" },
186
+ { "matches": "56", "name": "Passive Enroute Scan", "description": "A free scan as the parcel passes through a sorting plant, with no confirmation service attached" },
187
+ { "matches": "73", "name": "Insured Mail", "description": "Insured only, value over $50" },
188
+ { "matches": "82", "name": "Merchandise Return", "description": "With Delivery Confirmation" },
189
+ { "matches": "83", "name": "Merchandise Return", "description": "With Delivery Confirmation, insured value $50 or under" },
190
+ { "matches": "85", "name": "Merchandise Return", "description": "With Delivery Confirmation, insured value over $50" }
191
+ ]
192
+ }
193
+ ]
170
194
  },
171
195
  {
172
- "name": "USPS 91",
173
- "id": "usps_91",
174
- "description": "USPS now calls this the IMpd barcode format",
196
+ "name": "USPS IMpb C",
197
+ "id": "usps_impb_c",
198
+ "description": "IMpb constructs C01 through C10, plus USPS retail",
175
199
  "glossary": {
176
- "ApplicationIdentifier": { "description": "91 through 95, saying which USPS service and mailer ID scheme the rest belongs to." }
200
+ "ShipperId": { "description": "The company that holds the postal permit the package was mailed under, which Publication 199 calls the Mailer ID. Nine digits when it starts with 9, six otherwise." }
177
201
  },
178
202
  "regex": [
179
- "\\s*(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
203
+ "\\s*(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})(?<DestinationZipPlus4>([0-9]\\s*){4}(?=([0-9]\\s*){22}(?![0-9])))?)?",
180
204
  "(?<SerialNumber>",
181
- "(?<ApplicationIdentifier>9\\s*[12345]\\s*)?",
182
- "(?<SCNC>([0-9]\\s*){2})",
183
- "(?<ServiceType>([0-9]\\s*){2})",
184
- "(?<ShipperId>([0-9]\\s*){8})",
185
- "(?<PackageId>([0-9]\\s*){11}|([0-9]\\s*){7})",
205
+ "(?<ApplicationIdentifier>9\\s*2\\s*(?=([0-9]\\s*){3}9)|9\\s*3\\s*(?=([0-9]\\s*){3}[0-8])|9\\s*5\\s*)",
206
+ "(?<ServiceType>([0-9]\\s*){3})",
207
+ "(?=9\\s*([0-9]\\s*){8}(([0-9]\\s*){11}|([0-9]\\s*){7})[0-9]\\s*(?![0-9])",
208
+ "|[0-8]\\s*([0-9]\\s*){5}(([0-9]\\s*){14}|([0-9]\\s*){10})[0-9]\\s*(?![0-9]))",
209
+ "(?<ShipperId>9\\s*([0-9]\\s*){8}|[0-8]\\s*([0-9]\\s*){5})",
210
+ "(?<PackageId>([0-9]\\s*){14}|([0-9]\\s*){11}|([0-9]\\s*){10}|([0-9]\\s*){7})",
186
211
  ")",
187
212
  "(?<CheckDigit>[0-9]\\s*)"
188
213
  ],
@@ -193,74 +218,35 @@
193
218
  "evens_multiplier": 3,
194
219
  "odds_multiplier": 1,
195
220
  "reverse": false
196
- },
197
- "serial_number_format": {
198
- "prepend_if": {
199
- "matches_regex": "^(?!9[1-5]).+",
200
- "content": "91"
201
- }
202
- }
203
- },
204
- "partners": [{
205
- "description": "FedEx SmartPost uses USPS for last mile delivery, but not all USPS91 numbers are SmartPosts",
206
- "partner_type": "shipper",
207
- "partner_id": "fedex_smartpost",
208
- "validation": {
209
- "matches_all": [
210
- {
211
- "regex_group_name": "ServiceType",
212
- "matches": "29"
213
- },
214
- {
215
- "matches": "61",
216
- "regex_group_name": "SCNC"
217
- },
218
- {
219
- "regex_group_name": "ApplicationIdentifier",
220
- "matches": "92"
221
- }
222
- ]
223
221
  }
224
222
  },
225
- {
226
- "description": "DHL uses USPS for last mile delivery, but not all USPS91 numbers are DHL Ecommerce",
227
- "partner_type": "shipper",
228
- "partner_id": "dhl_ecommerce_30",
229
- "validation": {
230
- "matches_all": [
231
- {
232
- "regex_group_name": "ServiceType",
233
- "matches": "29"
234
- },
235
- {
236
- "matches": "61",
237
- "regex_group_name": "SCNC"
238
- },
239
- {
240
- "matches": "420",
241
- "regex_group_name": "RoutingApplicationId"
242
- }
243
- ]
244
- }
245
- }],
246
223
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
247
224
  "test_numbers": {
248
225
  "valid": [
249
- "420 22153 9101026837331000039521",
250
- "7196 9010 7560 0307 7385",
226
+ "4201002334249200190132607600833457",
227
+ "4201028200009261290113185417468510",
228
+ " 4 2 0 1 0 2 8 2 0 0 0 0 9 2 6 1 2 9 0 1 1 3 1 8 5 4 1 7 4 6 8 5 1 0 ",
251
229
  "9505 5110 6960 5048 6006 24",
252
- "9101 1234 5678 9000 0000 13",
253
230
  "92748931507708513018050063",
254
231
  "92001903060085300042901077",
255
- "9400 1112 0108 0805 4830 16",
256
232
  "9361 2898 7870 0317 6337 95",
257
- "9405803699300124287899"
233
+ "9261292700768711948021",
234
+ "420 11213 92 6129098349792366623 8",
235
+ "92 6129098349792366623 8",
236
+ "420902459261290336128704042634",
237
+ "420941179261290336128704062441",
238
+ "420926029261290336128704067248",
239
+ "420902729261290336128704124163",
240
+ "420950549261290336128704133837",
241
+ "420193809261290336128704280838",
242
+ "420900469261290336128704216936"
258
243
  ],
259
244
  "invalid": [
260
245
  "61299998820821171811",
261
246
  "9200000000000000000000",
262
247
  "420000000000000000000000000000",
263
- "420000009200000000000000000000"
248
+ "420000009200000000000000000000",
249
+ "4201028200009261290113185417468511"
264
250
  ]
265
251
  },
266
252
  "additional": [
@@ -268,14 +254,16 @@
268
254
  "name": "Service Type",
269
255
  "regex_group_name": "ServiceType",
270
256
  "lookup": [
271
- {
272
- "matches": "11",
273
- "name": "First Class (R)"
274
- },
275
- {
276
- "matches": "29",
277
- "name": "Fedex Smart Post"
278
- }
257
+ { "matches": "612", "name": "Parcel Select" }
258
+ ]
259
+ },
260
+ {
261
+ "name": "Application Identifier",
262
+ "regex_group_name": "ApplicationIdentifier",
263
+ "lookup": [
264
+ { "matches": "92", "name": "Commercial mailer", "description": "Nine digit mailer ID, and may have been carried by another company before USPS" },
265
+ { "matches": "93", "name": "Commercial mailer", "description": "Six digit mailer ID, and may have been carried by another company before USPS" },
266
+ { "matches": "95", "name": "USPS Retail", "description": "Issued at a post office counter, an automated postal center, or a self-service kiosk" }
279
267
  ]
280
268
  }
281
269
  ]
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "Yodel",
3
+ "courier_code": "yodel",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "Yodel",
7
+ "id": "yodel",
8
+ "description": "Yodel's UK domestic numbers, carried on a JD or JJD prefix. Numbers entering the Yodel network from other carriers can take other formats.",
9
+ "regex": [
10
+ "\\s*J\\s*(J\\s*)?D\\s*(?<SerialNumber>([0-9]\\s*){16})\\s*"
11
+ ],
12
+ "validation": {},
13
+ "tracking_url": "https://www.yodel.co.uk/tracking/%s",
14
+ "test_numbers": {
15
+ "valid": [
16
+ "JJD0002257639032011",
17
+ "JJD0002255380054291",
18
+ "JD0002242617644732",
19
+ "JD0002254405060992",
20
+ "J J D 0 0 0 2 2 5 7 6 3 9 0 3 2 0 2 0"
21
+ ],
22
+ "invalid": [
23
+ "JD000225440506099",
24
+ "JC0002254405060992"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -1,3 +1,3 @@
1
1
  module TrackingNumber
2
- VERSION = "2.5.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -60,4 +60,29 @@ module TrackingNumber
60
60
  def self.new(tracking_number)
61
61
  detect(tracking_number)
62
62
  end
63
+
64
+ # Renamed in tracking_number_data 2.0. The old constant still describes the same barcode.
65
+ RENAMED_TYPES = { USPS91: :USPSIMpbC, USPS22: :USPSIMpbN, USPS34v2: :USPSIMpbC }.freeze
66
+
67
+ # Removed in tracking_number_data 2.0. An alias would answer is_a? for every IMpb number,
68
+ # which is the claim the data stopped making, so these raise instead.
69
+ WITHDRAWN_TYPES = {
70
+ FedExSmartPost: 'USPSIMpbC',
71
+ DHLECommerce30: 'USPSIMpbC'
72
+ }.freeze
73
+
74
+ def self.const_missing(name)
75
+ if (renamed = RENAMED_TYPES[name])
76
+ warn "TrackingNumber::#{name} is now TrackingNumber::#{renamed}"
77
+ return const_get(renamed)
78
+ end
79
+
80
+ if (replacement = WITHDRAWN_TYPES[name])
81
+ raise NameError, "TrackingNumber::#{name} no longer exists. These are USPS IMpb barcodes " \
82
+ "carrying nothing that identifies the shipper, so they now decode as " \
83
+ "TrackingNumber::#{replacement}."
84
+ end
85
+
86
+ super
87
+ end
63
88
  end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ # No shipped definition declares partners currently at the tracking number data 2.0 release,
4
+ # but the concept is still supported in the data spec, and should be tested, so we're faking it here
5
+
6
+ class PartnershipTest < Minitest::Test
7
+ NUMBER = 'ZZ1234567890'.freeze
8
+
9
+ def setup
10
+ @shipper = build_class('test_shipper', partner_id: 'test_carrier', partner_type: 'carrier')
11
+ @carrier = build_class('test_carrier', partner_id: 'test_shipper', partner_type: 'shipper')
12
+
13
+ TrackingNumber::TYPES.push(@shipper, @carrier)
14
+ end
15
+
16
+ def teardown
17
+ TrackingNumber::TYPES.delete(@shipper)
18
+ TrackingNumber::TYPES.delete(@carrier)
19
+ end
20
+
21
+ should "report a partnership from both sides" do
22
+ assert @shipper.new(NUMBER).partnership?
23
+ assert @carrier.new(NUMBER).partnership?
24
+ end
25
+
26
+ should "pair each side with the other" do
27
+ partners = @carrier.new(NUMBER).partners
28
+
29
+ assert_instance_of @shipper, partners.shipper
30
+ assert_instance_of @carrier, partners.carrier
31
+ end
32
+
33
+ should "report which side of the partnership each is" do
34
+ assert @shipper.new(NUMBER).shipper?
35
+ assert !@shipper.new(NUMBER).carrier?
36
+
37
+ assert @carrier.new(NUMBER).carrier?
38
+ assert !@carrier.new(NUMBER).shipper?
39
+ end
40
+
41
+ should "only match the carrier side when searching by default" do
42
+ matches = TrackingNumber.search("shipped as #{NUMBER} today")
43
+
44
+ assert_equal [@carrier], matches.collect(&:class)
45
+ end
46
+
47
+ should "match both sides when searching for all" do
48
+ matches = TrackingNumber.search("shipped as #{NUMBER} today", match: :all)
49
+
50
+ assert_equal [@shipper, @carrier], matches.collect(&:class)
51
+ end
52
+
53
+ private
54
+
55
+ def build_class(id, partner_id:, partner_type:)
56
+ pattern = 'Z\s*Z\s*(?<SerialNumber>([0-9]\s*){10})'
57
+
58
+ klass = Class.new(TrackingNumber::Base)
59
+ klass.const_set('ID', id)
60
+ klass.const_set('COURIER_CODE', id.to_sym)
61
+ klass.const_set('COURIER_INFO', name: id, courier_code: id.to_sym)
62
+ klass.const_set('SEARCH_PATTERN', Regexp.new("\\b#{pattern}\\b"))
63
+ klass.const_set('VERIFY_PATTERN', Regexp.new("^#{pattern}$"))
64
+ klass.const_set('VALIDATION', {})
65
+ klass.const_set('ADDITIONAL', nil)
66
+ klass.const_set('TRACKING_URL', nil)
67
+ klass.const_set('PARTNERS', [{ partner_id: partner_id, partner_type: partner_type }])
68
+
69
+ klass
70
+ end
71
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class RenamedTypesTest < Minitest::Test
4
+ NUMBER = '420112139261290983497923666238'.freeze
5
+
6
+ should "resolve a renamed constant to the definition that replaced it" do
7
+ assert_equal TrackingNumber::USPSIMpbC, silenced { TrackingNumber::USPS91 }
8
+ assert_equal TrackingNumber::USPSIMpbN, silenced { TrackingNumber::USPS22 }
9
+ assert_equal TrackingNumber::USPSIMpbC, silenced { TrackingNumber::USPS34v2 }
10
+ end
11
+
12
+ should "keep is_a? answering for a renamed constant" do
13
+ assert TrackingNumber.new(NUMBER).is_a?(silenced { TrackingNumber::USPS91 })
14
+ end
15
+
16
+ should "warn once a renamed constant is reached for" do
17
+ _, err = capture_io { TrackingNumber::USPS91 }
18
+
19
+ assert_match(/USPS91 is now TrackingNumber::USPSIMpbC/, err)
20
+ end
21
+
22
+ # An alias would answer is_a? for every IMpb number, including labels these carriers never touched
23
+ should "refuse a withdrawn constant and name its replacement" do
24
+ error = assert_raises(NameError) { TrackingNumber::FedExSmartPost }
25
+ assert_match(/no longer exists/, error.message)
26
+ assert_match(/USPSIMpbC/, error.message)
27
+
28
+ assert_raises(NameError) { TrackingNumber::DHLECommerce30 }
29
+ end
30
+
31
+ should "leave an unrelated missing constant alone" do
32
+ error = assert_raises(NameError) { TrackingNumber::NotADefinition }
33
+
34
+ assert_match(/uninitialized constant/, error.message)
35
+ end
36
+
37
+ private
38
+
39
+ def silenced
40
+ resolved = nil
41
+ capture_io { resolved = yield }
42
+ resolved
43
+ end
44
+ end
data/test/test_helper.rb CHANGED
@@ -50,10 +50,11 @@ class Minitest::Test
50
50
  assert !t.valid?
51
51
  end
52
52
 
53
- def should_fail_on_check_digit_changes(valid_number)
53
+ def should_fail_on_check_digit_changes(valid_number, klass)
54
54
  digits = valid_number.gsub(/\s/, "").chars.compact.to_a
55
- last = digits.pop.to_i
56
- digits << (last <= 2 ? last + 3 : last - 3).to_s
55
+ position = digits.join.match(klass::VERIFY_PATTERN).begin("CheckDigit")
56
+ current = digits[position].to_i
57
+ digits[position] = (current <= 2 ? current + 3 : current - 3).to_s
57
58
  invalid_number = digits.join
58
59
  t = TrackingNumber.new(invalid_number)
59
60
  assert !t.valid?, "#{invalid_number} reported as a valid #{t.class}, and it shouldn't be"
@@ -30,7 +30,7 @@ class TrackingNumberMetaTest < Minitest::Test
30
30
  if tracking_info[:validation][:checksum]
31
31
  # only run this test if number format has checksum
32
32
  should "fail on check digit changes with #{valid_number}" do
33
- should_fail_on_check_digit_changes(valid_number)
33
+ should_fail_on_check_digit_changes(valid_number, klass)
34
34
  end
35
35
  end
36
36
 
@@ -109,7 +109,7 @@ class TrackingNumberTest < Minitest::Test
109
109
  end
110
110
 
111
111
  should "report correct service" do
112
- assert_nil tracking_number.service_type
112
+ assert_equal "Delivery Confirmation", tracking_number.service_type
113
113
  end
114
114
 
115
115
  should "report correct shipper_id" do
@@ -142,7 +142,7 @@ class TrackingNumberTest < Minitest::Test
142
142
  end
143
143
 
144
144
  should "report correct shipper_id" do
145
- assert_equal "00190132", tracking_number.shipper_id
145
+ assert_equal "901326076", tracking_number.shipper_id
146
146
  end
147
147
 
148
148
  should "report correct no destination" do
@@ -158,7 +158,7 @@ class TrackingNumberTest < Minitest::Test
158
158
  end
159
159
 
160
160
  should "report no partners" do
161
- assert_equal nil, tracking_number.partners
161
+ assert_nil tracking_number.partners
162
162
  end
163
163
 
164
164
  should "report as shipper and carrier" do
@@ -172,7 +172,7 @@ class TrackingNumberTest < Minitest::Test
172
172
  end
173
173
  end
174
174
 
175
- context "tracking number partnership data for FedExSmartPost/USPS91" do
175
+ context "tracking number additional data for USPS Parcel Select" do
176
176
  tracking_number = TrackingNumber.new("420 11213 92 6129098349792366623 8")
177
177
 
178
178
  should "report correct courier name" do
@@ -184,31 +184,28 @@ class TrackingNumberTest < Minitest::Test
184
184
  end
185
185
 
186
186
  should "report correct service type" do
187
- assert_equal "Fedex Smart Post", tracking_number.service_type
188
- end
189
-
190
- should "report partnership" do
191
- assert_equal true, tracking_number.partnership?
187
+ assert_equal "Parcel Select", tracking_number.service_type
192
188
  end
193
189
 
194
- should "report not shipper side of the partnership" do
195
- assert_equal false, tracking_number.shipper?
190
+ should "report no partnership" do
191
+ assert_equal false, tracking_number.partnership?
196
192
  end
197
193
 
198
- should "report carrier side of the partnership" do
194
+ should "report as shipper and carrier" do
195
+ assert_equal true, tracking_number.shipper?
199
196
  assert_equal true, tracking_number.carrier?
200
197
  end
201
198
 
202
- should "report partner pairing" do
203
- assert_equal :fedex, tracking_number.partners.shipper.courier_code
199
+ should "report no partners" do
200
+ assert_nil tracking_number.partners
204
201
  end
205
202
  end
206
203
 
207
- context "searching numbers that have partners" do
208
- partnership_number = "420 11213 92 6129098349792366623 8"
204
+ context "searching for numbers" do
205
+ parcel_select_number = "420 11213 92 6129098349792366623 8"
209
206
  single_number = "92001903060085300042901077"
210
207
 
211
- search_string = ["number that matches two services", partnership_number, " number that matches only one: ", single_number, "let's see if that does it"].join(' ')
208
+ search_string = ["number that matches two services", parcel_select_number, " number that matches only one: ", single_number, "let's see if that does it"].join(' ')
212
209
 
213
210
  should "match only carriers by default" do
214
211
  matches = TrackingNumber.search(search_string)
@@ -218,7 +215,7 @@ class TrackingNumberTest < Minitest::Test
218
215
 
219
216
  should "match all if specified" do
220
217
  matches = TrackingNumber.search(search_string, match: :all)
221
- assert_equal 3, matches.size
218
+ assert_equal 2, matches.size
222
219
  end
223
220
 
224
221
  should "match only shippers if specified" do
@@ -53,6 +53,7 @@ Gem::Specification.new do |s|
53
53
  s.add_runtime_dependency('activesupport', '>= 4.2.5')
54
54
  s.add_runtime_dependency('json', '>= 1.8.3')
55
55
  s.add_development_dependency('activemodel', '> 4.2.5.1')
56
+ s.add_development_dependency('irb')
56
57
  s.add_development_dependency('minitest', '~> 5.5')
57
58
  s.add_development_dependency('minitest-reporters', '~> 1.1')
58
59
  s.add_development_dependency('rake', '~> 13.0')
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: 2.5.0
4
+ version: 3.0.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: 2026-08-13 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 4.2.5.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: irb
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: minitest
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -175,6 +189,7 @@ files:
175
189
  - lib/data/couriers/speedee.json
176
190
  - lib/data/couriers/ups.json
177
191
  - lib/data/couriers/usps.json
192
+ - lib/data/couriers/yodel.json
178
193
  - lib/data/couriers/yunexpress.json
179
194
  - lib/tracking_number.rb
180
195
  - lib/tracking_number/active_model_validator.rb
@@ -187,6 +202,8 @@ files:
187
202
  - lib/tracking_number/version.rb
188
203
  - package.json
189
204
  - test/active_model_validator_test.rb
205
+ - test/partnership_test.rb
206
+ - test/renamed_types_test.rb
190
207
  - test/test_helper.rb
191
208
  - test/tracking_number_meta_test.rb
192
209
  - test/tracking_number_test.rb