Module: Models::TaxableResource

Extended by:
ActiveSupport::Concern
Included in:
CreditMemo, Invoice, Order, Quote
Defined in:
app/concerns/models/taxable_resource.rb

Belongs to collapse

Instance Method Summary collapse

Instance Method Details

#apply_tax_rate_to_line_itemsObject

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.



179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/concerns/models/taxable_resource.rb', line 179

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_paramsObject



117
118
119
120
121
122
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 117

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_linesObject



258
259
260
261
262
263
# File 'app/concerns/models/taxable_resource.rb', line 258

def calculate_tax_for_all_lines
  line_items.each do |li|
    li.calculate_tax
    li.save
  end
end

#copy_tax_rate(orig_rate) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'app/concerns/models/taxable_resource.rb', line 67

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_dateObject



30
31
32
# File 'app/concerns/models/taxable_resource.rb', line 30

def effective_date
  try(:tax_date) || try(:shipped_date) || Date.current
end

#get_rates_for_line(line_item) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/concerns/models/taxable_resource.rb', line 212

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) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'app/concerns/models/taxable_resource.rb', line 147

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_goodsObject



194
195
196
197
198
# File 'app/concerns/models/taxable_resource.rb', line 194

def manual_rate_goods
  return nil unless try(:manual_tax_rate_goods)

  manual_tax_rate_goods / 100
end

#manual_rate_servicesObject



200
201
202
203
204
# File 'app/concerns/models/taxable_resource.rb', line 200

def manual_rate_services
  return nil unless try(:manual_tax_rate_services)

  manual_tax_rate_services / 100
end

#manual_rate_shippingObject



206
207
208
209
210
# File 'app/concerns/models/taxable_resource.rb', line 206

def manual_rate_shipping
  return nil unless try(:manual_tax_rate_shipping)

  manual_tax_rate_shipping / 100
end

#origin_addressObject



34
35
36
# File 'app/concerns/models/taxable_resource.rb', line 34

def origin_address
  store&.warehouse_address
end

#refresh_tax_rateObject



170
171
172
173
# File 'app/concerns/models/taxable_resource.rb', line 170

def refresh_tax_rate
  get_tax_rate(auto_save: true)
  apply_tax_rate_to_line_items
end

#resource_not_taxable?Boolean

Returns:

  • (Boolean)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'app/concerns/models/taxable_resource.rb', line 81

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_rateResourceTaxRate



9
# File 'app/concerns/models/taxable_resource.rb', line 9

belongs_to :resource_tax_rate, dependent: :destroy, optional: true

#set_initial_tax_rateObject



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'app/concerns/models/taxable_resource.rb', line 38

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

Returns:

  • (Boolean)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'app/concerns/models/taxable_resource.rb', line 15

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_codeObject



105
106
107
108
109
# File 'app/concerns/models/taxable_resource.rb', line 105

def state_code
  return nil if shipping_address.nil?

  shipping_address.state_code
end

#state_code_symObject



111
112
113
114
115
# File 'app/concerns/models/taxable_resource.rb', line 111

def state_code_sym
  return nil if state_code.nil?

  state_code.to_sym
end

#taxes_grouped_by_rateObject



250
251
252
253
254
255
256
# File 'app/concerns/models/taxable_resource.rb', line 250

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_typeObject



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/concerns/models/taxable_resource.rb', line 228

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