Class: TrackingNumberFormatValidator

Inherits:
ActiveModel::EachValidator
  • Object
show all
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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Examples:

validates :tracking_number, tracking_number_format: { carrier_method: :carrier }

See Also:

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's test_numbers.valid data. 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.rb enforces it.

{
  'Canpar' => 'D420339200000131714001',
  'UPS' => '1Z5R89390357567127',
  'FedEx' => '986578788855',
  'USPS' => '03071790000523483741',
  'Canadapost' => '0073938000549297',
  'Purolator' => '520343872765',
  'SpeedeeDelivery' => 'SP029692030001607875',
  'DhlExpress' => '3318810025'
}.freeze

Class Method Summary collapse

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.

Parameters:

  • carrier (String, nil)

Returns:

  • (Symbol, nil)


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

Parameters:

  • carrier (String, nil)

Returns:

  • (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.

Parameters:

  • carrier (String)

    canonical internal carrier name

  • value (String)
  • allow_override (Boolean) (defaults to: true)

    whether the owning form exposes the
    validation-bypass checkbox used by Shipment/Delivery forms

Returns:

  • (String)


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.message_for(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.

Parameters:

  • gem_code (Symbol)

    e.g. :canpar

Returns:



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.

Parameters:

  • carrier (String)

    canonical internal carrier name

  • value (String)

Returns:

  • (Boolean)


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.

Parameters:

  • record (ActiveRecord::Base)
  • attribute (Symbol)
  • value (String, nil)


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.message_for(carrier, value))
end