Skip to content

Commit

Permalink
feat(tax-integrations): Cover tax deduction case for tax integrations (
Browse files Browse the repository at this point in the history
…#2628)

## Context

Lago recently launched integration with tax provider Anrok.

## Description

In some scenarios tax is not applied on the sum of all fees (with coupon
applied).

Instead, there are scenarios where tax base is less than the sum of the
fees (with coupon applied) and it depends mostly on the country (e.g.
Texas in the USA).

This PR is covering described case. `tax_base_rate` is included in tax
logic and also used to display correct base in the UI/PDF.
  • Loading branch information
lovrocolic authored Sep 30, 2024
1 parent a8c3a93 commit 1754cda
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 32 deletions.
1 change: 1 addition & 0 deletions app/graphql/types/invoices/applied_taxes/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Object < Types::BaseObject
field :enumed_tax_code, Types::Invoices::AppliedTaxes::WholeInvoiceApplicableTaxCodeEnum, null: true
field :fees_amount_cents, GraphQL::Types::BigInt, null: false
field :invoice, Types::Invoices::Object, null: false
field :taxable_amount_cents, GraphQL::Types::BigInt, null: false

def enumed_tax_code
object.tax_code if object.applied_on_whole_invoice?
Expand Down
1 change: 1 addition & 0 deletions app/models/fee.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def has_charge_filters?
# refunded_at :datetime
# succeeded_at :datetime
# taxes_amount_cents :bigint not null
# taxes_base_rate :float default(1.0), not null
# taxes_precise_amount_cents :decimal(40, 15) default(0.0), not null
# taxes_rate :float default(0.0), not null
# total_aggregated_units :decimal(, )
Expand Down
34 changes: 22 additions & 12 deletions app/models/invoice/applied_tax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AppliedTax < ApplicationRecord

monetize :amount_cents,
:fees_amount_cents,
:taxable_amount_cents,
with_model_currency: :amount_currency

validates :amount_cents, numericality: {greater_than_or_equal_to: 0}
Expand All @@ -21,25 +22,34 @@ class AppliedTax < ApplicationRecord
def applied_on_whole_invoice?
TAX_CODES_APPLICABLE_ON_WHOLE_INVOICE.include?(tax_code)
end

def taxable_amount_cents
base_amount = taxable_base_amount_cents

return fees_amount_cents if base_amount.blank? || base_amount.zero?

base_amount
end
end
end

# == Schema Information
#
# Table name: invoices_taxes
#
# id :uuid not null, primary key
# amount_cents :bigint default(0), not null
# amount_currency :string not null
# fees_amount_cents :bigint default(0), not null
# tax_code :string not null
# tax_description :string
# tax_name :string not null
# tax_rate :float default(0.0), not null
# created_at :datetime not null
# updated_at :datetime not null
# invoice_id :uuid not null
# tax_id :uuid
# id :uuid not null, primary key
# amount_cents :bigint default(0), not null
# amount_currency :string not null
# fees_amount_cents :bigint default(0), not null
# tax_code :string not null
# tax_description :string
# tax_name :string not null
# tax_rate :float default(0.0), not null
# taxable_base_amount_cents :bigint default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# invoice_id :uuid not null
# tax_id :uuid
#
# Indexes
#
Expand Down
10 changes: 8 additions & 2 deletions app/services/credit_notes/apply_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ def call
result.applied_taxes << applied_tax

base_amount_cents = compute_base_amount_cents(tax_code)
applied_tax.base_amount_cents = base_amount_cents.round
applied_tax.base_amount_cents = (base_amount_cents.round * taxes_base_rate(invoice_applied_tax)).round

tax_amount_cents = (base_amount_cents * invoice_applied_tax.tax_rate).fdiv(100)
tax_amount_cents = (applied_tax.base_amount_cents * invoice_applied_tax.tax_rate).fdiv(100)
applied_tax.amount_cents = tax_amount_cents.round

applied_taxes_amount_cents += tax_amount_cents
Expand Down Expand Up @@ -102,5 +102,11 @@ def pro_rated_taxes_rate(applied_tax)
def find_invoice_applied_tax(tax_code)
invoice.applied_taxes.find_by(tax_code: tax_code)
end

def taxes_base_rate(applied_tax)
return 1 if applied_tax.fees_amount_cents.blank? || applied_tax.fees_amount_cents.zero?

applied_tax.taxable_amount_cents.fdiv(applied_tax.fees_amount_cents)
end
end
end
19 changes: 17 additions & 2 deletions app/services/fees/apply_provider_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def call
applied_taxes_amount_cents = 0
applied_precise_taxes_amount_cents = 0.to_d
applied_taxes_rate = 0
taxes_base_rate = taxes_base_rate(fee_taxes.tax_breakdown.first)

fee_taxes.tax_breakdown.each do |tax|
tax_rate = tax.rate.to_f * 100
Expand All @@ -29,8 +30,8 @@ def call
)
fee.applied_taxes << applied_tax

tax_amount_cents = (fee.sub_total_excluding_taxes_amount_cents * tax_rate).fdiv(100)
tax_precise_amount_cents = (fee.sub_total_excluding_taxes_precise_amount_cents * tax_rate).fdiv(100.to_d)
tax_amount_cents = (fee.sub_total_excluding_taxes_amount_cents * taxes_base_rate * tax_rate).fdiv(100)
tax_precise_amount_cents = (fee.sub_total_excluding_taxes_precise_amount_cents * taxes_base_rate * tax_rate).fdiv(100.to_d)

applied_tax.amount_cents = tax_amount_cents.round
applied_tax.precise_amount_cents = tax_precise_amount_cents
Expand All @@ -46,6 +47,7 @@ def call
fee.taxes_amount_cents = applied_taxes_amount_cents.round
fee.taxes_precise_amount_cents = applied_precise_taxes_amount_cents
fee.taxes_rate = applied_taxes_rate
fee.taxes_base_rate = taxes_base_rate

result
rescue ActiveRecord::RecordInvalid => e
Expand All @@ -55,5 +57,18 @@ def call
private

attr_reader :fee, :fee_taxes

def taxes_base_rate(tax)
return 1 unless tax

tax_rate = tax.rate.to_f * 100
tax_amount_cents = (fee.sub_total_excluding_taxes_amount_cents * tax_rate).fdiv(100)

if tax.tax_amount < tax_amount_cents
tax.tax_amount.fdiv(tax_amount_cents)
else
1
end
end
end
end
9 changes: 8 additions & 1 deletion app/services/invoices/apply_provider_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def call

tax_amount_cents = compute_tax_amount_cents(tax)
applied_tax.fees_amount_cents = fees_amount_cents(tax)
applied_tax.taxable_base_amount_cents = taxable_base_amount_cents(tax)&.round
applied_tax.amount_cents = tax_amount_cents.round

# NOTE: when applied on user current usage, the invoice is
Expand Down Expand Up @@ -92,7 +93,7 @@ def compute_tax_amount_cents(tax)
key = calculate_key(tax)

indexed_fees[key]
.sum { |fee| fee.sub_total_excluding_taxes_amount_cents * tax.rate.to_f }
.sum { |fee| fee.sub_total_excluding_taxes_amount_cents * fee.taxes_base_rate * tax.rate.to_f }
end

def pro_rated_taxes_rate(tax)
Expand All @@ -115,6 +116,12 @@ def fees_amount_cents(tax)
indexed_fees[key].sum(&:sub_total_excluding_taxes_amount_cents)
end

def taxable_base_amount_cents(tax)
key = calculate_key(tax)

indexed_fees[key].sum { |fee| fee.sub_total_excluding_taxes_amount_cents * fee.taxes_base_rate }
end

def fetch_provider_taxes_result
taxes_result = if invoice.draft? || invoice.advance_charges?
Integrations::Aggregator::Taxes::Invoices::CreateDraftService.call(invoice:)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
- if applied_tax.applied_on_whole_invoice?
td.body-2 = I18n.t('invoice.tax_name_only.' + applied_tax.tax_code)
- else
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.taxable_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
Expand Down
2 changes: 1 addition & 1 deletion app/views/templates/invoices/v4/_subscription_details.slim
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
td.body-2 = I18n.t('invoice.tax_name_only.' + applied_tax.tax_code)
td.body-2
- else
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.taxable_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ table.total-table width="100%"
td.body-2
- else
td.body-2
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.taxable_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
Expand Down
2 changes: 1 addition & 1 deletion app/views/templates/invoices/v4/charge.slim
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ html
td.body-2
- else
td.body-2
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.taxable_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
Expand Down
2 changes: 1 addition & 1 deletion app/views/templates/invoices/v4/one_off.slim
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ html
td.body-2
- else
td.body-2
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.taxable_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class AddTaxesDeductionRateFieldsToFeesAndAppliedTaxes < ActiveRecord::Migration[7.1]
def change
add_column :fees, :taxes_base_rate, :float, default: 1.0, null: false
add_column :invoices_taxes, :taxable_base_amount_cents, :bigint, default: 0, null: false
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions spec/models/invoice/applied_tax_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,26 @@
end
end
end

describe '#taxable_amount_cents' do
before do
applied_tax.fees_amount_cents = 150
end

context 'when taxable_base_amount_cents is zero' do
it 'returns fees_amount_cents' do
applied_tax.taxable_base_amount_cents = 0

expect(applied_tax.taxable_amount_cents).to eq(150)
end
end

context 'when taxable_base_amount_cents is NOT zero' do
it 'returns taxable_base_amount_cents' do
applied_tax.taxable_base_amount_cents = 100

expect(applied_tax.taxable_amount_cents).to eq(100)
end
end
end
end
30 changes: 30 additions & 0 deletions spec/services/fees/apply_provider_taxes_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@
expect(fee).to have_attributes(taxes_amount_cents: 170, taxes_precise_amount_cents: 170.0, taxes_rate: 17)
end
end

context 'when there is tax deduction' do
let(:fee_taxes) do
OpenStruct.new(
tax_breakdown: [
OpenStruct.new(name: 'tax 2', type: 'type2', rate: '0.12', tax_amount: 96),
OpenStruct.new(name: 'tax 3', type: 'type3', rate: '0.05', tax_amount: 40)
]
)
end

it 'creates applied_taxes based on the provider taxes' do
result = apply_service.call

aggregate_failures do
expect(result).to be_success

applied_taxes = result.applied_taxes
expect(applied_taxes.count).to eq(2)

expect(applied_taxes.map(&:tax_code)).to contain_exactly('tax_2', 'tax_3')
expect(fee).to have_attributes(
taxes_amount_cents: 136,
taxes_precise_amount_cents: 136.0,
taxes_rate: 17,
taxes_base_rate: 0.8
)
end
end
end
end

context 'when fee already have taxes' do
Expand Down
Loading

0 comments on commit 1754cda

Please sign in to comment.