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
-
#resource_tax_rate ⇒ ResourceTaxRate?
Tax-rate snapshot for this document.
Instance Method Summary collapse
-
#apply_tax_rate_to_line_items ⇒ true
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
Whether the document is exempt from taxation: missing/unsupported destination, effective exemption certificate, manual tax-exempt flag, store transfer, or US↔CA cross-border order.
-
#set_initial_tax_rate ⇒ ResourceTaxRate?
before_create hook — populates the document's ResourceTaxRate.
-
#should_refresh_tax_rate? ⇒ Boolean
after_update guard — whether the document's tax rate must be re-fetched (shipping address, tax-exempt flag, or a manual rate changed).
-
#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 ⇒ true
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.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'app/concerns/models/taxable_resource.rb', line 251 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 } # rubocop:disable Style/KeywordArgumentsMerging 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 })) } # rubocop:enable Style/KeywordArgumentsMerging 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.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'app/concerns/models/taxable_resource.rb', line 172 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.
365 366 367 368 369 370 |
# File 'app/concerns/models/taxable_resource.rb', line 365 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.
104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'app/concerns/models/taxable_resource.rb', line 104 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.
48 49 50 |
# File 'app/concerns/models/taxable_resource.rb', line 48 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).
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'app/concerns/models/taxable_resource.rb', line 304 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.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'app/concerns/models/taxable_resource.rb', line 212 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.
273 274 275 276 277 |
# File 'app/concerns/models/taxable_resource.rb', line 273 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.
282 283 284 285 286 |
# File 'app/concerns/models/taxable_resource.rb', line 282 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.
291 292 293 294 295 |
# File 'app/concerns/models/taxable_resource.rb', line 291 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.
57 58 59 |
# File 'app/concerns/models/taxable_resource.rb', line 57 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.
240 241 242 243 |
# File 'app/concerns/models/taxable_resource.rb', line 240 def refresh_tax_rate get_tax_rate(auto_save: true) apply_tax_rate_to_line_items end |
#resource_not_taxable? ⇒ Boolean
Whether the document is exempt from taxation: missing/unsupported
destination, effective exemption certificate, manual tax-exempt flag,
store transfer, or US↔CA cross-border order.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'app/concerns/models/taxable_resource.rb', line 123 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?
Returns tax-rate snapshot for this document.
17 |
# File 'app/concerns/models/taxable_resource.rb', line 17 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.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'app/concerns/models/taxable_resource.rb', line 68 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
after_update guard — whether the document's tax rate must be re-fetched
(shipping address, tax-exempt flag, or a manual rate changed). Always
false for CI-invoice credit orders, whose tax comes from the marketplace.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'app/concerns/models/taxable_resource.rb', line 28 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.
150 151 152 153 154 |
# File 'app/concerns/models/taxable_resource.rb', line 150 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.
159 160 161 162 163 |
# File 'app/concerns/models/taxable_resource.rb', line 159 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.
352 353 354 355 356 357 358 |
# File 'app/concerns/models/taxable_resource.rb', line 352 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.
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'app/concerns/models/taxable_resource.rb', line 326 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 |