Module: Models::TaxableResource
- Extended by:
- ActiveSupport::Concern
- Included in:
- CreditMemo, Invoice, Order, Quote
- Defined in:
- app/concerns/models/taxable_resource.rb
Overview
Mixin for Order, Quote, Invoice and CreditMemo that owns the
document-level ResourceTaxRate (sales-tax / VAT / GST snapshot for
the document) and propagates it to LineItems. The rate is fetched
from the external tax service (Taxes::GetTaxRate, which talks to
TaxJar / Avalara) when the document is taxable, copied from a parent
document when there's a credit / RMA chain, or zeroed out for store
transfers, tax-exempt billers and CI marketplace invoices.
Belongs to collapse
Instance Method Summary collapse
-
#apply_tax_rate_to_line_items ⇒ Object
Propagates the current resource_tax_rate to all line items without re-fetching from the external tax service.
-
#build_tax_params ⇒ Hash{Symbol => Object}
Builds the parameter hash sent to Taxes::GetTaxRate (which wraps the tax-service API call).
-
#calculate_tax_for_all_lines ⇒ void
Re-runs Models::TaxableLine#calculate_tax against every line on the document and saves it.
-
#copy_tax_rate(orig_rate) ⇒ ResourceTaxRate?
Clones a parent document's ResourceTaxRate onto this document so invoiced/credited amounts match the original sale, even if the underlying tax tables have shifted in the meantime.
-
#effective_date ⇒ Date
Date the tax rate is evaluated against —
tax_date(e.g. for back-dated invoices) when present, then ship date, falling back to today for in-flight cart/quote. -
#get_rates_for_line(line_item) ⇒ Hash{Symbol => Object}
Resolves the rate / tax-type / resource-rate-id for a given LineItem based on its
tax_class(g/svc/shp/none), honouring any manual rate override on the document. -
#get_tax_rate(auto_save: true) ⇒ ResourceTaxRate?
Fetches a fresh ResourceTaxRate from the tax service via Taxes::GetTaxRate for the document's origin / destination / line-items, persists it (as a build-or-update) and assigns it onto the resource.
-
#manual_rate_goods ⇒ BigDecimal?
Manual goods tax rate (decimal form) when the rep has overridden the auto-fetched rate on the document; nil when the auto rate should win.
-
#manual_rate_services ⇒ BigDecimal?
Manual services tax rate (decimal form), nil when not overridden.
-
#manual_rate_shipping ⇒ BigDecimal?
Manual shipping tax rate (decimal form), nil when not overridden.
-
#origin_address ⇒ Address?
The fulfilling warehouse address — origin side of the nexus/destination-tax calculation.
-
#refresh_tax_rate ⇒ void
after_update hook (when #should_refresh_tax_rate? fires) — fetches a fresh rate from the tax service and pushes it down to every line item.
- #resource_not_taxable? ⇒ Boolean
-
#set_initial_tax_rate ⇒ ResourceTaxRate?
before_create hook — populates the document's ResourceTaxRate.
- #should_refresh_tax_rate? ⇒ Boolean
-
#state_code ⇒ String?
Convenience accessor for the destination state's two-letter code.
-
#state_code_sym ⇒ Symbol?
#state_code as a Symbol — matches the keys used by
TAXABLE_STATES. -
#taxes_grouped_by_rate ⇒ Hash{String => BigDecimal}
Compact tax-totals roll-up —
{ 'sales_tax' => 123.45, … }— used by reports and the audit-trail diff viewer. -
#taxes_grouped_by_type ⇒ Hash{String => Hash{Symbol => Object}}
PDF-friendly tax breakdown grouped by
tax_typeand rate —{ 'sales_tax' => { tax_amount:, name:, rate: }, … }with $0 rates filtered out and the legacytax_offsetrounding adjustment added to the first bucket so totals reconcile with old data.
Instance Method Details
#apply_tax_rate_to_line_items ⇒ Object
Propagates the current resource_tax_rate to all line items without re-fetching
from the external tax service. Use this when the tax rate has already been
inherited (e.g. copied from the original invoice) and only the line items need
to be updated to reflect it.
238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'app/concerns/models/taxable_resource.rb', line 238 def apply_tax_rate_to_line_items # Reload to avoid acting on lines pending destroy (frozen hash errors). line_items.reload if resource_tax_rate.nil? line_items.each { |li| li.update_tax_rate(rate: 0, resource_tax_rate_id: nil, tax_type: nil) } else attrs = { resource_tax_rate_id: resource_tax_rate.id, tax_type: resource_tax_rate.tax_type } line_items.active_goods_lines.each { |li| li.update_tax_rate(**attrs.merge({ rate: manual_rate_goods || resource_tax_rate.rate_goods })) } line_items.active_services_lines.each { |li| li.update_tax_rate(**attrs.merge({ rate: manual_rate_services || resource_tax_rate.rate_services })) } line_items.active_shipping_lines.each { |li| li.update_tax_rate(**attrs.merge({ rate: manual_rate_shipping || resource_tax_rate.rate_shipping })) } line_items.active_tax_unclassed_lines.each { |li| li.update_tax_rate(rate: 0, resource_tax_rate_id: nil, tax_type: nil) } end true end |
#build_tax_params ⇒ Hash{Symbol => Object}
Builds the parameter hash sent to Taxes::GetTaxRate (which wraps
the tax-service API call). Includes origin / destination address
parts, the line-item breakdown (eager-loading item.product_tax_code
to avoid N+1) and the freight charged. Used both for getting rates
and for posting transactions to TaxJar.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'app/concerns/models/taxable_resource.rb', line 161 def build_tax_params params = { effective_date:, from_street: origin_address.street1, from_city: origin_address.city, from_state: origin_address.state_code, from_zip: origin_address.zip, from_country: origin_address.country&.iso, to_street: shipping_address.street1, to_city: shipping_address.city, to_state: shipping_address.state_code, to_zip: shipping_address.zip, to_country: shipping_address.country&.iso } if respond_to?(:line_items) # Add line items break down, first we check if our association is already loaded, if not we will eager_load with item for speed line_items_loaded = line_items.loaded? ? line_items : line_items.eager_load(item: :product_tax_code) params[:line_items] = line_items_loaded.active_non_shipping_lines.map do |li| { id: li.id, quantity: li.quantity, product_tax_code: li.product_tax_code&.product_tax_code, tax_class: li.tax_class, unit_price: li.discounted_price, # Not sure if they want discount or unit price msrp here discount: li.discounts_total # This is total discount on the line apparently } end params[:freight_charged] = if respond_to?(:shipping_cost) shipping_cost else 0.0 end end params end |
#calculate_tax_for_all_lines ⇒ void
This method returns an undefined value.
Re-runs Models::TaxableLine#calculate_tax against every line on
the document and saves it. Used after a manual-rate change or a
rate refresh to bring stale tax_total columns in line.
350 351 352 353 354 355 |
# File 'app/concerns/models/taxable_resource.rb', line 350 def calculate_tax_for_all_lines line_items.each do |li| li.calculate_tax li.save end end |
#copy_tax_rate(orig_rate) ⇒ ResourceTaxRate?
Clones a parent document's ResourceTaxRate onto this document so
invoiced/credited amounts match the original sale, even if the
underlying tax tables have shifted in the meantime. Builds-or-updates
the existing resource_tax_rate row and assigns it back.
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'app/concerns/models/taxable_resource.rb', line 98 def copy_tax_rate(orig_rate) return nil if orig_rate.nil? new_rate = resource_tax_rate || ResourceTaxRate.new new_rate.assign_attributes( source: orig_rate.source, tax_type: orig_rate.tax_type, tax_rate_id: orig_rate.tax_rate_id, rate_goods: orig_rate.rate_goods, rate_services: orig_rate.rate_services, rate_shipping: orig_rate.rate_shipping, retrieved_on: orig_rate.retrieved_on ) new_rate.save! self.resource_tax_rate = new_rate new_rate end |
#effective_date ⇒ Date
Date the tax rate is evaluated against — tax_date (e.g. for
back-dated invoices) when present, then ship date, falling back
to today for in-flight cart/quote.
42 43 44 |
# File 'app/concerns/models/taxable_resource.rb', line 42 def effective_date try(:tax_date) || try(:shipped_date) || Date.current end |
#get_rates_for_line(line_item) ⇒ Hash{Symbol => Object}
Resolves the rate / tax-type / resource-rate-id for a given
LineItem based on its tax_class (g / svc / shp / none),
honouring any manual rate override on the document. Returns zero
rate when the resource has no resource_tax_rate (untaxable).
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'app/concerns/models/taxable_resource.rb', line 289 def get_rates_for_line(line_item) return { rate: 0, resource_tax_rate_id: nil, tax_type: nil } if resource_tax_rate.nil? || line_item.tax_class == 'none' rate = case line_item.tax_class when 'svc' manual_rate_services || resource_tax_rate.rate_services when 'shp' manual_rate_shipping || resource_tax_rate.rate_shipping else manual_rate_goods || resource_tax_rate.rate_goods end rate ||= 0 # Roman added because errors reported when picking items. I.e. TO662607 { rate:, resource_tax_rate_id: resource_tax_rate.id, tax_type: resource_tax_rate.tax_type } end |
#get_tax_rate(auto_save: true) ⇒ ResourceTaxRate?
Fetches a fresh ResourceTaxRate from the tax service via
Taxes::GetTaxRate for the document's origin / destination /
line-items, persists it (as a build-or-update) and assigns it onto
the resource. When the resource isn't taxable
(#resource_not_taxable?) the existing rate is destroyed and the
FK column blanked. Set auto_save: false from before_create so
the parent save's transaction owns the writes.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'app/concerns/models/taxable_resource.rb', line 201 def get_tax_rate(auto_save: true) if resource_not_taxable? # delete any existing rate resource_tax_rate&.destroy self.resource_tax_rate = nil update_columns(resource_tax_rate_id: nil) if auto_save nil else res = Taxes::GetTaxRate.new.process(**build_tax_params) # create or update the linked resource_tax_rate new_rate = resource_tax_rate || ResourceTaxRate.new new_rate.assign_attributes( source: res.source, tax_type: res.tax_type, tax_rate_id: res.tax_rate_id, rate_goods: res.rate_goods, rate_services: res.rate_services, rate_shipping: res.rate_shipping, retrieved_on: res.retrieved_on ) new_rate.save! self.resource_tax_rate = new_rate update_columns(resource_tax_rate_id: new_rate.id) if auto_save new_rate end end |
#manual_rate_goods ⇒ BigDecimal?
Manual goods tax rate (decimal form) when the rep has overridden
the auto-fetched rate on the document; nil when the auto rate
should win.
258 259 260 261 262 |
# File 'app/concerns/models/taxable_resource.rb', line 258 def manual_rate_goods return nil unless try(:manual_tax_rate_goods) manual_tax_rate_goods / 100 end |
#manual_rate_services ⇒ BigDecimal?
Manual services tax rate (decimal form), nil when not overridden.
267 268 269 270 271 |
# File 'app/concerns/models/taxable_resource.rb', line 267 def manual_rate_services return nil unless try(:manual_tax_rate_services) manual_tax_rate_services / 100 end |
#manual_rate_shipping ⇒ BigDecimal?
Manual shipping tax rate (decimal form), nil when not overridden.
276 277 278 279 280 |
# File 'app/concerns/models/taxable_resource.rb', line 276 def manual_rate_shipping return nil unless try(:manual_tax_rate_shipping) manual_tax_rate_shipping / 100 end |
#origin_address ⇒ Address?
The fulfilling warehouse address — origin side of the
nexus/destination-tax calculation. Pulled from the document's
Store's warehouse.
51 52 53 |
# File 'app/concerns/models/taxable_resource.rb', line 51 def origin_address store&.warehouse_address end |
#refresh_tax_rate ⇒ void
This method returns an undefined value.
after_update hook (when #should_refresh_tax_rate? fires) — fetches
a fresh rate from the tax service and pushes it down to every line
item. Triggered on shipping-address / tax-exempt / manual-rate changes.
229 230 231 232 |
# File 'app/concerns/models/taxable_resource.rb', line 229 def refresh_tax_rate get_tax_rate(auto_save: true) apply_tax_rate_to_line_items end |
#resource_not_taxable? ⇒ Boolean
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'app/concerns/models/taxable_resource.rb', line 112 def resource_not_taxable? # no shipping address return true if shipping_address.nil? # no country associated with shipping address return true if shipping_address.country.nil? return true if shipping_address.country.iso == 'US' && TAXABLE_STATES.select { |k, _v| k == shipping_address.state_code.upcase.to_sym }.keys.map(&:to_s).first.nil? # they have a tax exemption return true if billing_entity && billing_entity.tax_exemptions.effective_on(effective_date).where(state_code:).any? # it's marked as manually tax exempt (e.g Amazon Seller Central order where Amazon has collected their exemption certificate) return true if respond_to?(:tax_exempt) && tax_exempt? # it's a store transfer return true if is_a?(Order) && (order_type == Order::STORE_TRANSFER) # it's an international order between US and CA return true if origin_address.country.present? && shipping_address.country.present? && origin_address.country.iso.in?(%w[US CA]) && (origin_address.country.iso != shipping_address.country.iso) # else it is taxable false end |
#resource_tax_rate ⇒ ResourceTaxRate
16 |
# File 'app/concerns/models/taxable_resource.rb', line 16 belongs_to :resource_tax_rate, dependent: :destroy, optional: true |
#set_initial_tax_rate ⇒ ResourceTaxRate?
before_create hook — populates the document's ResourceTaxRate.
Order-of-precedence: copy from the parent Order (for invoices),
from the original invoice (for RMAs and credit memos), from the
credit-Order (for stand-alone credit memos), otherwise call
#get_tax_rate to fetch a fresh rate from the tax service.
62 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/concerns/models/taxable_resource.rb', line 62 def set_initial_tax_rate if is_a?(Invoice) && order.present? # copy tax rate from order copy_tax_rate(order.resource_tax_rate) elsif is_a?(Order) && (order_type == 'CO') && rma.present? if rma.has_replacement_items? && rma.original_invoice&.invoice_type != Invoice::CI # If there is a replacement order, use current tax rates so the credit # and replacement orders line up. Skipped for CI (Consignment) invoices # since the marketplace (e.g. Amazon Seller Central) sets the tax and # we inherit those exact amounts on the credit order's line items — the # header rate must come from the invoice so it matches those amounts. get_tax_rate(auto_save: false) elsif rma.original_invoice.present? # copy tax rate from original invoice copy_tax_rate(rma.original_invoice.resource_tax_rate) else get_tax_rate(auto_save: false) end elsif is_a?(CreditMemo) && credit_order.present? # copy tax rate from credit order copy_tax_rate(credit_order.resource_tax_rate) elsif is_a?(CreditMemo) && original_invoice.present? # copy tax rate from original invoice copy_tax_rate(original_invoice.resource_tax_rate) else get_tax_rate(auto_save: false) end end |
#should_refresh_tax_rate? ⇒ Boolean
22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'app/concerns/models/taxable_resource.rb', line 22 def should_refresh_tax_rate? # CI = Consignment Invoice (marketplace sales, e.g. Amazon Seller Central). # On these invoices the marketplace collects and remits tax, so the tax_total # on each line item is set directly by the marketplace feed — not calculated # from our tax rates. Credit orders created from CI-invoice RMAs inherit those # exact amounts. Refreshing would overwrite them with a rate-based calculation. return false if respond_to?(:ci_invoice_credit_order?) && ci_invoice_credit_order? saved_change_to_shipping_address_id? or (respond_to?(:tax_exempt) and saved_change_to_tax_exempt?) or (respond_to?(:manual_tax_rate_goods) and saved_change_to_manual_tax_rate_goods?) or (respond_to?(:manual_tax_rate_services) and saved_change_to_manual_tax_rate_services?) or (respond_to?(:manual_tax_rate_shipping) and saved_change_to_manual_tax_rate_shipping?) end |
#state_code ⇒ String?
Convenience accessor for the destination state's two-letter code.
139 140 141 142 143 |
# File 'app/concerns/models/taxable_resource.rb', line 139 def state_code return nil if shipping_address.nil? shipping_address.state_code end |
#state_code_sym ⇒ Symbol?
#state_code as a Symbol — matches the keys used by TAXABLE_STATES.
148 149 150 151 152 |
# File 'app/concerns/models/taxable_resource.rb', line 148 def state_code_sym return nil if state_code.nil? state_code.to_sym end |
#taxes_grouped_by_rate ⇒ Hash{String => BigDecimal}
Compact tax-totals roll-up — { 'sales_tax' => 123.45, … } —
used by reports and the audit-trail diff viewer.
337 338 339 340 341 342 343 |
# File 'app/concerns/models/taxable_resource.rb', line 337 def taxes_grouped_by_rate tax_grouped = {} line_items.group_by(&:tax_type).each do |tax_type, lines| tax_grouped[tax_type] = lines.sum(&:tax_total) unless tax_type.nil? end tax_grouped end |
#taxes_grouped_by_type ⇒ Hash{String => Hash{Symbol => Object}}
PDF-friendly tax breakdown grouped by tax_type and rate —
{ 'sales_tax' => { tax_amount:, name:, rate: }, … } with $0
rates filtered out and the legacy tax_offset rounding adjustment
added to the first bucket so totals reconcile with old data.
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'app/concerns/models/taxable_resource.rb', line 311 def taxes_grouped_by_type tax_grouped = {} tt = nil grouped_by_type = line_items.all.group_by(&:tax_type) grouped_by_type.delete_if { |tax_type, _lines| tax_type.nil? } grouped_by_type.each do |tax_type, lines_by_type| lines_by_type.group_by(&:tax_rate).each do |rate, lines_by_rate| tt = tax_type tax_amount = lines_by_rate.sum(&:tax_total) rate_percentage = (rate * 100) next if tax_amount.zero? # don't show $0 amounts rate_string = ActionController::Base.helpers.number_with_precision(rate_percentage, precision: 3, strip_insignificant_zeros: true) name = "#{TaxRate.description_for(tt, destination_country: try(:shipping_address)&.country)} #{rate_string}%" tax_grouped[tax_type] = { tax_amount:, name:, rate: rate_percentage } end end # add any offset amount to the total, this is for legacy documents which had a rounding issue tax_grouped[tt][:tax_amount] += tax_offset if tt && tax_grouped[tt] && tax_offset tax_grouped end |