Class: TaxRate

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/tax_rate.rb

Overview

== Schema Information

Table name: tax_rates
Database name: primary

id :integer not null, primary key
country_iso :string not null
description :string(255)
effective_date :date not null
expiration_date :date
goods :decimal(6, 4)
services :decimal(6, 4)
shipping :decimal(6, 4)
short_name :string(255)
state_code :string
tax_type :string(3) not null
use_food :decimal(6, 4)
use_merch :decimal(6, 4)
sales_tax_credit_account_id :integer
sales_tax_payable_account_id :integer
use_tax_account_id :integer

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#effective_dateObject (readonly)



69
# File 'app/models/tax_rate.rb', line 69

validates :goods, :services, :shipping, :effective_date, :tax_type, presence: true

#goodsObject (readonly)



67
# File 'app/models/tax_rate.rb', line 67

validates :goods, :services, :shipping, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0 }

#servicesObject (readonly)



67
# File 'app/models/tax_rate.rb', line 67

validates :goods, :services, :shipping, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0 }

#shippingObject (readonly)



67
# File 'app/models/tax_rate.rb', line 67

validates :goods, :services, :shipping, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0 }

#state_codeObject (readonly)

validates_presence_of :sales_tax_payable_account_id Per Venus request on July 2023

Validations:

  • Presence ({ if: proc { |tr| tr.country_iso.in?(%(US CA)) } })


72
# File 'app/models/tax_rate.rb', line 72

validates :state_code, presence: { if: proc { |tr| tr.country_iso.in?(%(US CA)) } }

#tax_typeObject (readonly)



69
# File 'app/models/tax_rate.rb', line 69

validates :goods, :services, :shipping, :effective_date, :tax_type, presence: true

#use_foodObject (readonly)



68
# File 'app/models/tax_rate.rb', line 68

validates :use_merch, :use_food, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0, allow_nil: true }

#use_merchObject (readonly)



68
# File 'app/models/tax_rate.rb', line 68

validates :use_merch, :use_food, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0, allow_nil: true }

#use_tax_account_idObject (readonly)



70
# File 'app/models/tax_rate.rb', line 70

validates :use_tax_account_id, presence: { if: proc { |tr| tr.use_merch.present? or tr.use_food.present? } }

Class Method Details

.description_for(tax_type, destination_country: nil) ⇒ String

Human description for tax_type. Falls back to "VAT <country>" for EU rates and "Sales Tax" when no row in
TAXABLE_STATES matches.

Parameters:

  • tax_type (String)
  • destination_country (Country, nil) (defaults to: nil)

Returns:

  • (String)


145
146
147
148
149
150
151
152
# File 'app/models/tax_rate.rb', line 145

def self.description_for(tax_type, destination_country: nil)
  d = TAXABLE_STATES.find { |_k, v| v[:tax_type] == tax_type }.try(:[], 1).try(:[], :description)
  if d.nil? && tax_type == 'vat'
    d = +"VAT"
    d << " #{destination_country.name}" if destination_country
  end
  d || 'Sales Tax'
end

.effective_nowActiveRecord::Relation<TaxRate>

A relation of TaxRates that are effective now. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



61
# File 'app/models/tax_rate.rb', line 61

scope :effective_now, -> { effective_on(Date.current) }

.effective_onActiveRecord::Relation<TaxRate>

A relation of TaxRates that are effective on. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



60
# File 'app/models/tax_rate.rb', line 60

scope :effective_on, ->(date) { where("effective_date <= :date and (expiration_date IS NULL OR expiration_date >= :date)", date: date) }

.ransackable_scopes(_auth_object = nil) ⇒ Array<Symbol>

Returns Ransack-allowlisted scopes.

Returns:

  • (Array<Symbol>)

    Ransack-allowlisted scopes



127
128
129
# File 'app/models/tax_rate.rb', line 127

def self.ransackable_scopes(_auth_object = nil)
  %i[effective_on]
end

.rates_for_voucher_entryArray<Hash>

Flatten every effective-today TaxRate into one row per
(rate_type, jurisdiction) for the voucher / AP-bill entry
form's tax dropdown. tax_type is "V" for value-added /
sales tax and "U" for use-tax rows.

Returns:

  • (Array<Hash>)

    each {"label", "value", "rate", "id", "tax_type"}



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'app/models/tax_rate.rb', line 90

def self.rates_for_voucher_entry
  rates = []
  TaxRate.effective_on(Date.current).each do |tr|
    %w[goods services shipping].each do |rate_type|
      next unless tr.send(rate_type).present? && (tr.send(rate_type) != 0)

      rates << if tr.state_code.present?
                 { "label" => "#{tr.tax_type.upcase} #{rate_type} #{tr.state_code}", "value" => "#{tr.tax_type.upcase}_#{rate_type}_#{tr.state_code}", "rate" => tr.send(rate_type) * 100, "id" => tr.id, "tax_type" => "V" }
               else
                 { "label" => "#{tr.tax_type.upcase} #{rate_type} #{tr.country_iso}", "value" => "#{tr.tax_type.upcase}_#{rate_type}_#{tr.country_iso}", "rate" => tr.send(rate_type) * 100, "id" => tr.id, "tax_type" => "V" }
               end
    end
    %w[use_food use_merch].each do |rate_type|
      next unless tr.send(rate_type).present? && (tr.send(rate_type) != 0)

      rates << if tr.state_code.present?
                 { "label" => "#{tr.tax_type.upcase} #{rate_type} #{tr.state_code}", "value" => "#{tr.tax_type.upcase}_#{rate_type}_#{tr.state_code}", "rate" => tr.send(rate_type) * 100, "id" => tr.id, "tax_type" => "U" }
               else
                 { "label" => "#{tr.tax_type.upcase} #{rate_type} #{tr.country_iso}", "value" => "#{tr.tax_type.upcase}_#{rate_type}_#{tr.country_iso}", "rate" => tr.send(rate_type) * 100, "id" => tr.id, "tax_type" => "U" }
               end
    end
  end
  rates
end

.sales_taxActiveRecord::Relation<TaxRate>

A relation of TaxRates that are sales tax. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



64
# File 'app/models/tax_rate.rb', line 64

scope :sales_tax, -> { where.not(sales_tax_payable_account_id: nil) }

.sales_tax_creditActiveRecord::Relation<TaxRate>

A relation of TaxRates that are sales tax credit. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



62
# File 'app/models/tax_rate.rb', line 62

scope :sales_tax_credit, -> { where.not(sales_tax_credit_account_id: nil) }

.tax_types_for_selectArray<String>

Distinct tax_type codes from TAXABLE_STATES plus "vat",
for the tax-type filter dropdown.

Returns:

  • (Array<String>)


134
135
136
# File 'app/models/tax_rate.rb', line 134

def self.tax_types_for_select
  TAXABLE_STATES.map { |_k, v| v[:tax_type].to_s }.uniq + ['vat']
end

.taxjar_summary_rates(country_code: nil) ⇒ Object

Returns an array of tax rate for a specific country, this is designed for single tax rate
regardless of origin/destination, such as in canada
{
:country_code => "CA",
:country => "Canada",
:region_code => "NB",
:region => "New Brunswick",
:minimum_rate => {
:label => "GST",
:rate => 0.05
},
:average_rate => {
:label => "HST",
:rate => 0.15
}
}



171
172
173
174
175
# File 'app/models/tax_rate.rb', line 171

def self.taxjar_summary_rates(country_code: nil)
  rates = Api::Taxjar.client.summary_rates
  rates = rates.select { |r| r.country_code == country_code } if country_code
  rates
end

.use_internal_ratesActiveRecord::Relation<TaxRate>

A relation of TaxRates that are use internal rates. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



65
# File 'app/models/tax_rate.rb', line 65

scope :use_internal_rates, ->(states) { where(state_code: states) }

.use_taxActiveRecord::Relation<TaxRate>

A relation of TaxRates that are use tax. Active Record Scope

Returns:

  • (ActiveRecord::Relation<TaxRate>)

See Also:



63
# File 'app/models/tax_rate.rb', line 63

scope :use_tax, -> { where.not(use_tax_account_id: nil) }

Instance Method Details

#active?Boolean

Whether the rate is currently effective (today between
effective_date and expiration_date).

Returns:

  • (Boolean)


80
81
82
# File 'app/models/tax_rate.rb', line 80

def active?
  effective_date <= Date.current and expiration_date > Date.current
end

#countryCountry

Returns:

See Also:



55
# File 'app/models/tax_rate.rb', line 55

belongs_to :country, foreign_key: 'country_iso', primary_key: 'iso', optional: true

#expiration_date_after_effective_datevoid (protected)

This method returns an undefined value.

Validation: an expiration_date, if present, can't precede
effective_date.



189
190
191
# File 'app/models/tax_rate.rb', line 189

def expiration_date_after_effective_date
  errors.add(:expiration_date, "cannot be before effective date") if expiration_date && effective_date && (expiration_date < effective_date)
end

#has_overlapping_tax_ratesvoid (protected)

This method returns an undefined value.

Validation: prevent two rates from covering the same
(country, state, tax_type) over an overlapping date window.
NULL expiration_date is treated as "open-ended".



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'app/models/tax_rate.rb', line 197

def has_overlapping_tax_rates
  return unless effective_date && tax_type

  # Overlap requires the same country + state + tax_type. Without the
  # country_iso filter, different-country rates that share an empty
  # state_code (e.g. EU VAT) were wrongly flagged as conflicts.
  scope = TaxRate.where(country_iso: country_iso, state_code: state_code, tax_type: tax_type)
  scope = scope.where.not(id: id) if persisted?

  # Two date ranges overlap when each starts on or before the other ends.
  # A NULL expiration_date means "open-ended" / no upper bound, so both the
  # stored rate and the record being validated may be unbounded on the right.
  #   overlap ⇔ r.effective_date <= new.expiration AND new.effective_date <= r.expiration_date
  scope = scope.where(tax_rates: { effective_date: ..expiration_date }) if expiration_date
  scope = scope.where('tax_rates.expiration_date IS NULL OR tax_rates.expiration_date >= ?', effective_date)

  conflicting_ids = scope.ids
  return if conflicting_ids.empty?

  errors.add(:base, "Existing tax rates with id: #{conflicting_ids.join(',')} in place for these values and with the same date coverage. Cannot save an overlapping tax rate.")
end

#resource_tax_ratesActiveRecord::Relation<ResourceTaxRate>

Returns:

See Also:



58
# File 'app/models/tax_rate.rb', line 58

has_many :resource_tax_rates

#sales_tax_credit_accountLedgerDetailAccount



53
# File 'app/models/tax_rate.rb', line 53

belongs_to :sales_tax_credit_account, class_name: "LedgerDetailAccount", optional: true

#sales_tax_payable_accountLedgerDetailAccount



54
# File 'app/models/tax_rate.rb', line 54

belongs_to :sales_tax_payable_account, class_name: "LedgerDetailAccount", optional: true

#stateState

Returns:

See Also:



51
# File 'app/models/tax_rate.rb', line 51

belongs_to :state, foreign_key: 'state_code', primary_key: 'code', optional: true

#tax_explanationString

"U" for use-tax rates, "V" for VAT / sales-tax rates —
the abbreviation shown next to the rate in the voucher form.

Returns:

  • (String)


118
119
120
121
122
123
124
# File 'app/models/tax_rate.rb', line 118

def tax_explanation
  if .nil?
    "V"
  else
    "U"
  end
end

#tax_type_available_in_statevoid (protected)

This method returns an undefined value.

Validation: the il (Illinois) tax type is only valid when
state_code == 'IL'.



182
183
184
# File 'app/models/tax_rate.rb', line 182

def tax_type_available_in_state
  errors.add(:tax_type, "Tax Type is not available in this state") if (state_code != 'IL') && (tax_type == 'il')
end

#use_tax_accountLedgerDetailAccount



52
# File 'app/models/tax_rate.rb', line 52

belongs_to :use_tax_account, class_name: "LedgerDetailAccount", optional: true

#voucher_itemsActiveRecord::Relation<VoucherItem>

Returns:

See Also:



57
# File 'app/models/tax_rate.rb', line 57

has_many :voucher_items