Class: TrackingNumberFormatValidator
- Inherits:
-
ActiveModel::EachValidator
- Object
- ActiveModel::EachValidator
- TrackingNumberFormatValidator
- Defined in:
- app/validators/tracking_number_format_validator.rb
Overview
Rejects a hand-keyed tracking number whose format can't belong to the
carrier it was entered against.
Motivating case: a Canpar label bought outside Heatwave (ShipStation,
which the Canada warehouse is required to use) carries two barcodes.
Only the 22-character D-prefixed one is trackable; the 16-digit one
next to it is not. Warehouse staff typed the 16-digit form into 33
Amazon.ca deliveries in 90 days, which left the CRM tracking link dead,
ShipEngine's webhook subscription with nothing to report, and the
shipment stuck at not_yet_in_system forever. The same class of typo
shows up as truncated UPS numbers (17 chars instead of 18) and FedEx
numbers pasted with spaces.
How it decides:
- Resolve the declared carrier to a canonical internal name with the
NAME-based Heatwave::Normalizers.shipping_carrier — never
resolve_shipping_carrier, which falls back to sniffing the
tracking number itself and would therefore always agree with it. - Look up that carrier's catalog courier_code in
PARCEL_CARRIER_INTERNAL_TO_TRACKING_NUMBER_GEM. No entry (LTL,
"Override", "Standard", blank) → no opinion, no error. - Marketplace channels (TRACKING_NUMBER_MARKETPLACE_CARRIERS)
accept when any catalog spec validates the number — the stored
carrier is the sales channel; the barcode is UPS / FedEx / USPS /
Amazon Logistics. - Otherwise accept when the value satisfies any barcode spec
Heatwave::TrackingNumber holds for that carrier — format AND checksum,
so single-character typos on
UPS / FedEx Ground / USPS IMpb / Canada Post are caught too.
Only runs when the attribute is actually changing, so historical bad
numbers don't block unrelated edits to old records.
Overridable: every validated model carries a
skip_tracking_number_validation accessor, wired to a checkbox on the
ship-label and edit-tracking forms, for the case where a carrier issues
something our specs don't know about. The forms set it on the delivery;
DeliveriesController pushes it down to the shipments alongside the
carrier and tracking number it already copies there.
Constant Summary collapse
- EXAMPLES =
A known-good number per carrier, quoted back to the operator so the
error says what to look for on the label rather than just "invalid".
Every value is a real number that satisfies the carrier's own spec —
the Canpar one off the label that started this, the rest lifted from
the catalog'stest_numbers.validdata. Don't invent
replacements: most made-up numbers fail the carrier's check digit, and
an example that wouldn't itself pass validation is worse than none.
test/validators/tracking_number_format_validator_test.rbenforces it. { 'Canpar' => 'D420339200000131714001', 'UPS' => '1Z5R89390357567127', 'FedEx' => '986578788855', 'USPS' => '03071790000523483741', 'Canadapost' => '0073938000549297', 'Purolator' => '520343872765', 'SpeedeeDelivery' => 'SP029692030001607875', 'DhlExpress' => '3318810025' }.freeze
Class Method Summary collapse
-
.catalog_courier_code(carrier) ⇒ Symbol?
Catalog courier_code for a canonical internal name, including Amazon Logistics (
:amazon) which is excluded from the single-spec parcel map so marketplace numbers can be UPS/USPS as well as TBA. - .marketplace_carrier?(carrier) ⇒ Boolean
-
.message_for(carrier, value, allow_override: true) ⇒ String
Shared operator-facing validation message.
-
.specs_for(gem_code) ⇒ Array<Heatwave::TrackingNumber::Spec>
Barcode families Heatwave::TrackingNumber holds for a courier_code.
-
.valid_for_carrier?(carrier, value) ⇒ Boolean
Shared format decision for scalar Shipment fields and RMA tracking-number collections.
Instance Method Summary collapse
Class Method Details
.catalog_courier_code(carrier) ⇒ Symbol?
Catalog courier_code for a canonical internal name, including Amazon
Logistics (:amazon) which is excluded from the single-spec parcel map
so marketplace numbers can be UPS/USPS as well as TBA.
117 118 119 120 |
# File 'app/validators/tracking_number_format_validator.rb', line 117 def self.catalog_courier_code(carrier) PARCEL_CARRIER_INTERNAL_TO_TRACKING_NUMBER_GEM[carrier] || CARRIER_CODE_MAP.find { |row| row[:internal] == carrier }&.dig(:gem) end |
.marketplace_carrier?(carrier) ⇒ Boolean
107 108 109 |
# File 'app/validators/tracking_number_format_validator.rb', line 107 def self.marketplace_carrier?(carrier) TRACKING_NUMBER_MARKETPLACE_CARRIERS.include?(carrier) end |
.message_for(carrier, value, allow_override: true) ⇒ String
Shared operator-facing validation message.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'app/validators/tracking_number_format_validator.rb', line 129 def self.(carrier, value, allow_override: true) correction = if allow_override 'Check it against the label and correct it, or tick "Save tracking numbers without validating".' else 'Check the number against the carrier label and correct it.' end if (suggestion = Shipping::TrackingNumberRepair.new(carrier, value).suggestion) return "#{value} is not a valid #{carrier} tracking number — did you mean #{suggestion}? #{correction}" end if marketplace_carrier?(carrier) return "#{value} is not a valid tracking number for #{carrier} " \ '(these labels are UPS, FedEx, USPS, or Amazon Logistics). ' \ "#{correction}" end example = EXAMPLES[carrier] "#{value} is not a valid #{carrier} tracking number" \ "#{" (#{carrier} numbers look like #{example})" if example}. " \ "#{correction}" end |
.specs_for(gem_code) ⇒ Array<Heatwave::TrackingNumber::Spec>
Barcode families Heatwave::TrackingNumber holds for a courier_code.
86 87 88 |
# File 'app/validators/tracking_number_format_validator.rb', line 86 def self.specs_for(gem_code) Heatwave::TrackingNumber.specs_for(gem_code) end |
.valid_for_carrier?(carrier, value) ⇒ Boolean
Shared format decision for scalar Shipment fields and RMA tracking-number
collections. Unknown/non-parcel carriers remain permissive.
96 97 98 99 100 101 102 103 |
# File 'app/validators/tracking_number_format_validator.rb', line 96 def self.valid_for_carrier?(carrier, value) return Heatwave::TrackingNumber.parse(value).valid? if marketplace_carrier?(carrier) gem_code = catalog_courier_code(carrier) return true if gem_code.nil? Heatwave::TrackingNumber.valid_for?(gem_code, value) end |
Instance Method Details
#validate_each(record, attribute, value) ⇒ void
This method returns an undefined value.
71 72 73 74 75 76 77 78 79 80 |
# File 'app/validators/tracking_number_format_validator.rb', line 71 def validate_each(record, attribute, value) return if value.blank? return unless record.will_save_change_to_attribute?(attribute) return if skip?(record) carrier = declared_carrier(record) return if self.class.valid_for_carrier?(carrier, value) record.errors.add(attribute, self.class.(carrier, value)) end |