Class: State

Inherits:
ApplicationRecord show all
Defined in:
app/models/state.rb

Overview

== Schema Information

Table name: states
Database name: primary

id :integer not null, primary key
code :string(255) not null
country_iso3 :string(255) not null
name :string(255) not null
region :string

Indexes

index_states_on_code_and_country_iso3 (code,country_iso3) UNIQUE
index_states_on_country_iso3 (country_iso3)

Constant Summary collapse

MAIN_COUNTRIES_ISO3 =

Main countries iso3.

%w[USA CAN].freeze
US_CONTINENTAL_STATE_LIST =

Us continental state list.

%w[AL AR AZ CA CO CT DC DE FL GA IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC
SD TN TX UT VA VT WA WI WV WY].freeze
CA_MAIN_PROVINCES =

Ca main provinces.

%w[AB BC MB NB NS ON QC SK].freeze
CONTINENTAL_REGION_CODES =

Continental region codes.

[] + US_CONTINENTAL_STATE_LIST + CA_MAIN_PROVINCES

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Has many collapse

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#codeString (readonly)

Returns:

  • (String)


45
# File 'app/models/state.rb', line 45

validates :code, uniqueness: { scope: :country_iso3 }

#country_iso3String (readonly)

Returns:

  • (String)


49
# File 'app/models/state.rb', line 49

validates :country_iso3, presence: true

#nameString (readonly)

Returns:

  • (String)


47
# File 'app/models/state.rb', line 47

validates :name, presence: true

Class Method Details

.by_countryActiveRecord::Relation<State>

A relation of States that are by country. Active Record Scope

Returns:

  • (ActiveRecord::Relation<State>)

See Also:



41
# File 'app/models/state.rb', line 41

scope :by_country, ->(country_iso3) { where(country_iso3: country_iso3.upcase) }

.by_country_isoActiveRecord::Relation<State>

A relation of States that are by country iso. Active Record Scope

Returns:

  • (ActiveRecord::Relation<State>)

See Also:



42
# File 'app/models/state.rb', line 42

scope :by_country_iso, ->(country_iso) { joins(:country).where(countries: { iso: country_iso.upcase }) }

.canadaActiveRecord::Relation<State>

A relation of States that are canada. Active Record Scope

Returns:

  • (ActiveRecord::Relation<State>)

See Also:



40
# File 'app/models/state.rb', line 40

scope :canada, -> { where(country_iso3: 'CAN') }

.code_for_string(str, countries_iso3: MAIN_COUNTRIES_ISO3) ⇒ String?

Looks up the state code for the given state name string.
Searches through the given list of country ISO3 codes.
Returns nil if no match found.

Parameters:

  • str (String, nil)
  • countries_iso3 (Array<String>) (defaults to: MAIN_COUNTRIES_ISO3)

Returns:

  • (String, nil)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/models/state.rb', line 139

def self.code_for_string(str, countries_iso3: MAIN_COUNTRIES_ISO3)
  return nil if str.blank?

  subdivision_name = normalize_subdivision_name(str)
  code = nil
  countries_iso3.each do |iso3|
    next unless (c = ISO3166::Country.find_country_by_alpha3(iso3))

    # This code finder is case sensitive so doing our own
    # code = c.find_subdivision_by_name(str)&.code
    code = c.subdivisions.find do |sub_code, sub|
      normalize_subdivision_name(sub_code) == subdivision_name ||
        normalize_subdivision_name(sub.name) == subdivision_name ||
        [sub.unofficial_names].flatten.compact.any? { |n| normalize_subdivision_name(n) == subdivision_name }
    end&.first
    break if code
  end
  # if no matches, try our internal table
  code ||= State.where(country_iso3: countries_iso3).where(State[:name].matches(subdivision_name, nil, false)).first&.code
  code
end

.normalize_subdivision_name(str) ⇒ String

Normalizes a state/province string for comparison.

Transliterates accents to ASCII BEFORE stripping punctuation. The strip
removes every character outside A-Z and whitespace, so on an accented
name it deletes the letter outright rather than folding it — "Québec"
became "QUBEC" and matched nothing, which blanked state_code and failed
Amazon.ca order import with "State code can't be blank". Applied to both
sides of the comparison so an accented ISO name matches an ASCII input too.

Input is coerced to valid UTF-8 first: I18n.transliterate matches with a
UTF-8 regexp, so a non-UTF-8 string (ISO-8859-1 from an EDI feed) raises
Encoding::CompatibilityError and malformed bytes raise ArgumentError.
The old ASCII-only strip tolerated both, and this runs inside order import,
so it must degrade rather than raise.

Parameters:

  • str (String, nil)

Returns:

  • (String)

    normalized, e.g. "Québec" -> "QUEBEC", "ca." -> "CA"



178
179
180
181
182
# File 'app/models/state.rb', line 178

def self.normalize_subdivision_name(str)
  value = str.to_s
  value = value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '') unless value.encoding == Encoding::UTF_8
  I18n.transliterate(value.scrub('')).upcase.gsub(/[^A-Z\s]/, '').strip
end

.options_for_shipping_ratesArray<Array(String, String)>

Returns a list of state name/code pairs for all states except
territories, sorted by region, for use in shipping rate options.

Returns:

  • (Array<Array(String, String)>)


126
127
128
129
130
131
# File 'app/models/state.rb', line 126

def self.options_for_shipping_rates
  # .insert(-1,%w[CONTINENTAL CONTINENTAL])
  where.not(region: 'US - Territories').sort_by(&:region).to_a.map do |c|
    ["#{c.name} - #{c.region.split(' - ').second}", c.code]
  end
end

.sortedActiveRecord::Relation<State>

A relation of States that are sorted. Active Record Scope

Returns:

  • (ActiveRecord::Relation<State>)

See Also:



38
# File 'app/models/state.rb', line 38

scope :sorted, -> { order(:name) }

.state_ids_for_select(countries_iso: nil, grouped_by_country: false, locale: nil) ⇒ Array<Array<String, Integer, nil>>, Hash{String => Array<Array<String, Integer, nil>>}

Parameters:

  • countries_iso (Array<String>, Symbol, nil) (defaults to: nil)
  • grouped_by_country (Boolean) (defaults to: false)
  • locale (Symbol, nil) (defaults to: nil)

Returns:

  • (Array<Array<String, Integer, nil>>, Hash{String => Array<Array<String, Integer, nil>>})


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'app/models/state.rb', line 95

def self.state_ids_for_select(countries_iso: nil, grouped_by_country: false, locale: nil)
  locale ||= I18n.locale
  lang_code = locale.to_s.first(2)
  if countries_iso == :all
    grouped_by_country = true # force it otherwise makes no sense
    countries_iso = ISO3166::Country.codes
  elsif countries_iso.nil?
    countries_iso = %w[US CA]
  else
    countries_iso = [countries_iso].flatten.compact
  end
  grouped = {}
  ungrouped = countries_iso.flat_map do |iso|
    iso3166_country = if iso.size == 2
                        ISO3166::Country[iso]
                      elsif iso.size == 3
                        ISO3166::Country.find_country_by_alpha3(iso)
                      end
    next unless iso3166_country

    states_array = iso3166_country.subdivisions.map { |k, v| [v.translations[lang_code] || k, State.where(code: k).first&.id] } || []
    grouped_country_name = iso3166_country.translations[lang_code] || iso3166_country.iso_short_name
    grouped[grouped_country_name] = states_array unless states_array.empty?
    states_array
  end
  grouped_by_country ? grouped : ungrouped
end

.states_for_select(countries_iso: nil, grouped_by_country: false, locale: nil) ⇒ Array<Array<String>>, Hash{String => Array<Array<String>>}

Returns a list of states grouped by country or ungrouped,
filtered by the provided country ISO codes.

Parameters:

  • countries_iso (Array<String>, Symbol, nil) (defaults to: nil)
  • grouped_by_country (Boolean) (defaults to: false)
  • locale (Symbol, nil) (defaults to: nil)

Returns:

  • (Array<Array<String>>, Hash{String => Array<Array<String>>})


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'app/models/state.rb', line 63

def self.states_for_select(countries_iso: nil, grouped_by_country: false, locale: nil)
  locale ||= I18n.locale
  lang_code = locale.to_s.first(2)
  if countries_iso == :all
    grouped_by_country = true # force it otherwise makes no sense
    countries_iso = ISO3166::Country.codes
  elsif countries_iso.nil?
    countries_iso = %w[US CA]
  else
    countries_iso = [countries_iso].flatten.compact
  end
  grouped = {}
  ungrouped = countries_iso.flat_map do |iso|
    iso3166_country = if iso.size == 2
                        ISO3166::Country[iso]
                      elsif iso.size == 3
                        ISO3166::Country.find_country_by_alpha3(iso)
                      end
    next unless iso3166_country

    states_array = iso3166_country.subdivisions.map { |k, v| [v.translations[lang_code] || k, k] } || []
    grouped_country_name = iso3166_country.translations[lang_code] || iso3166_country.iso_short_name
    grouped[grouped_country_name] = states_array unless states_array.empty?
    states_array
  end
  grouped_by_country ? grouped : ungrouped
end

.usaActiveRecord::Relation<State>

A relation of States that are usa. Active Record Scope

Returns:

  • (ActiveRecord::Relation<State>)

See Also:



39
# File 'app/models/state.rb', line 39

scope :usa, -> { where(country_iso3: 'USA') }

Instance Method Details

#countryCountry?

Returns:



36
# File 'app/models/state.rb', line 36

belongs_to :country, foreign_key: 'country_iso3', primary_key: 'iso3', optional: true

#tax_exemptionsActiveRecord::Relation<TaxExemption>

Returns:



31
# File 'app/models/state.rb', line 31

has_many :tax_exemptions, foreign_key: 'state_code', primary_key: 'code'

#tax_ratesActiveRecord::Relation<TaxRate>

Returns:

  • (ActiveRecord::Relation<TaxRate>)


33
# File 'app/models/state.rb', line 33

has_many :tax_rates, foreign_key: 'state_code', primary_key: 'code'

#to_sString

Returns:

  • (String)


52
53
54
# File 'app/models/state.rb', line 52

def to_s
  name
end