Class: TaxRate
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- TaxRate
- 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
- #effective_date ⇒ Object readonly
- #goods ⇒ Object readonly
- #services ⇒ Object readonly
- #shipping ⇒ Object readonly
-
#state_code ⇒ Object
readonly
validates_presence_of :sales_tax_payable_account_id Per Venus request on July 2023.
- #tax_type ⇒ Object readonly
- #use_food ⇒ Object readonly
- #use_merch ⇒ Object readonly
- #use_tax_account_id ⇒ Object readonly
Belongs to collapse
- #country ⇒ Country
- #sales_tax_credit_account ⇒ LedgerDetailAccount
- #sales_tax_payable_account ⇒ LedgerDetailAccount
- #state ⇒ State
- #use_tax_account ⇒ LedgerDetailAccount
Methods included from Models::Auditable
Has many collapse
- #resource_tax_rates ⇒ ActiveRecord::Relation<ResourceTaxRate>
- #voucher_items ⇒ ActiveRecord::Relation<VoucherItem>
Class Method Summary collapse
-
.description_for(tax_type, destination_country: nil) ⇒ String
Human description for
tax_type. -
.effective_now ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are effective now.
-
.effective_on ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are effective on.
-
.ransackable_scopes(_auth_object = nil) ⇒ Array<Symbol>
Ransack-allowlisted scopes.
-
.rates_for_voucher_entry ⇒ Array<Hash>
Flatten every effective-today TaxRate into one row per
(rate_type, jurisdiction)for the voucher / AP-bill entry form's tax dropdown. -
.sales_tax ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are sales tax.
-
.sales_tax_credit ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are sales tax credit.
-
.tax_types_for_select ⇒ Array<String>
Distinct
tax_typecodes fromTAXABLE_STATESplus"vat", for the tax-type filter dropdown. -
.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 } }.
-
.use_internal_rates ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are use internal rates.
-
.use_tax ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are use tax.
Instance Method Summary collapse
-
#active? ⇒ Boolean
Whether the rate is currently effective (today between
effective_dateandexpiration_date). -
#expiration_date_after_effective_date ⇒ void
protected
Validation: an
expiration_date, if present, can't precedeeffective_date. -
#has_overlapping_tax_rates ⇒ void
protected
Validation: prevent two rates from covering the same
(country, state, tax_type)over an overlapping date window. -
#tax_explanation ⇒ String
"U"for use-tax rates,"V"for VAT / sales-tax rates — the abbreviation shown next to the rate in the voucher form. -
#tax_type_available_in_state ⇒ void
protected
Validation: the
il(Illinois) tax type is only valid whenstate_code == 'IL'.
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
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#effective_date ⇒ Object (readonly)
69 |
# File 'app/models/tax_rate.rb', line 69 validates :goods, :services, :shipping, :effective_date, :tax_type, presence: true |
#goods ⇒ Object (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 } |
#services ⇒ Object (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 } |
#shipping ⇒ Object (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_code ⇒ Object (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_type ⇒ Object (readonly)
69 |
# File 'app/models/tax_rate.rb', line 69 validates :goods, :services, :shipping, :effective_date, :tax_type, presence: true |
#use_food ⇒ Object (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_merch ⇒ Object (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_id ⇒ Object (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.
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_now ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are effective now. Active Record Scope
61 |
# File 'app/models/tax_rate.rb', line 61 scope :effective_now, -> { effective_on(Date.current) } |
.effective_on ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are effective on. Active Record Scope
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.
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_entry ⇒ Array<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.
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_tax ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are sales tax. Active Record Scope
64 |
# File 'app/models/tax_rate.rb', line 64 scope :sales_tax, -> { where.not(sales_tax_payable_account_id: nil) } |
.sales_tax_credit ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are sales tax credit. Active Record Scope
62 |
# File 'app/models/tax_rate.rb', line 62 scope :sales_tax_credit, -> { where.not(sales_tax_credit_account_id: nil) } |
.tax_types_for_select ⇒ Array<String>
Distinct tax_type codes from TAXABLE_STATES plus "vat",
for the tax-type filter dropdown.
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_rates ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are use internal rates. Active Record Scope
65 |
# File 'app/models/tax_rate.rb', line 65 scope :use_internal_rates, ->(states) { where(state_code: states) } |
.use_tax ⇒ ActiveRecord::Relation<TaxRate>
A relation of TaxRates that are use tax. Active Record Scope
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).
80 81 82 |
# File 'app/models/tax_rate.rb', line 80 def active? effective_date <= Date.current and expiration_date > Date.current end |
#country ⇒ Country
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_date ⇒ void (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_rates ⇒ void (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_rates ⇒ ActiveRecord::Relation<ResourceTaxRate>
58 |
# File 'app/models/tax_rate.rb', line 58 has_many :resource_tax_rates |
#sales_tax_credit_account ⇒ LedgerDetailAccount
53 |
# File 'app/models/tax_rate.rb', line 53 belongs_to :sales_tax_credit_account, class_name: "LedgerDetailAccount", optional: true |
#sales_tax_payable_account ⇒ LedgerDetailAccount
54 |
# File 'app/models/tax_rate.rb', line 54 belongs_to :sales_tax_payable_account, class_name: "LedgerDetailAccount", optional: true |
#state ⇒ State
51 |
# File 'app/models/tax_rate.rb', line 51 belongs_to :state, foreign_key: 'state_code', primary_key: 'code', optional: true |
#tax_explanation ⇒ String
"U" for use-tax rates, "V" for VAT / sales-tax rates —
the abbreviation shown next to the rate in the voucher form.
118 119 120 121 122 123 124 |
# File 'app/models/tax_rate.rb', line 118 def tax_explanation if use_tax_account_id.nil? "V" else "U" end end |
#tax_type_available_in_state ⇒ void (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_account ⇒ LedgerDetailAccount
52 |
# File 'app/models/tax_rate.rb', line 52 belongs_to :use_tax_account, class_name: "LedgerDetailAccount", optional: true |
#voucher_items ⇒ ActiveRecord::Relation<VoucherItem>
57 |
# File 'app/models/tax_rate.rb', line 57 has_many :voucher_items |