Skip to content

Commit

Permalink
feat(applied-taxes): add logic for creating applied taxes based on ex…
Browse files Browse the repository at this point in the history
…ternal provider (#2267)

## Context

Currently Anrok - tax tool integration is being added

## Description

This PR created applied taxes objects in Lago based on fetched Anrok
taxes.

Two new services are added: `Fees::ApplyProviderTaxesService` and
`Invoices::ApplyProviderTaxesService`.

This logic follows existing behaviour in `Fees::ApplyTaxesService` and
`Invoices::ApplyTaxesService` services. The only difference is that
source of taxes is not Lago app anymore but external provider.

New services are not used yet.
  • Loading branch information
lovrocolic authored Jul 15, 2024
1 parent d666a2a commit bdc6103
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 6 deletions.
2 changes: 1 addition & 1 deletion app/models/fee/applied_tax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ class AppliedTax < ApplicationRecord
include PaperTrailTraceable

belongs_to :fee
belongs_to :tax
belongs_to :tax, optional: true
end
end
2 changes: 1 addition & 1 deletion app/models/invoice/applied_tax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AppliedTax < ApplicationRecord
include PaperTrailTraceable

belongs_to :invoice
belongs_to :tax
belongs_to :tax, optional: true

monetize :amount_cents,
:fees_amount_cents,
Expand Down
53 changes: 53 additions & 0 deletions app/services/fees/apply_provider_taxes_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Fees
class ApplyProviderTaxesService < BaseService
def initialize(fee:, fee_taxes:)
@fee = fee
@fee_taxes = fee_taxes

super
end

def call
result.applied_taxes = []
return result if fee.applied_taxes.any?

applied_taxes_amount_cents = 0
applied_taxes_rate = 0

fee_taxes.tax_breakdown.each do |tax|
tax_rate = tax.rate.to_f * 100

applied_tax = Fee::AppliedTax.new(
tax_description: tax.type,
tax_code: tax.name.parameterize(separator: '_'),
tax_name: tax.name,
tax_rate: tax_rate,
amount_currency: fee.amount_currency
)
fee.applied_taxes << applied_tax

tax_amount_cents = tax.tax_amount
applied_tax.amount_cents = tax_amount_cents.round
applied_tax.save! if fee.persisted?

applied_taxes_amount_cents += tax_amount_cents
applied_taxes_rate += tax_rate

result.applied_taxes << applied_tax
end

fee.taxes_amount_cents = applied_taxes_amount_cents.round
fee.taxes_rate = applied_taxes_rate

result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

private

attr_reader :fee, :fee_taxes
end
end
130 changes: 130 additions & 0 deletions app/services/invoices/apply_provider_taxes_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# frozen_string_literal: true

module Invoices
class ApplyProviderTaxesService < BaseService
def initialize(invoice:)
@invoice = invoice
@provider_taxes = fetch_provider_taxes_result

super
end

def call
provider_taxes.raise_if_error!

result.applied_taxes = []
applied_taxes_amount_cents = 0
taxes_rate = 0

applicable_taxes.values.each do |tax|
tax_rate = tax.rate.to_f * 100

applied_tax = invoice.applied_taxes.new(
tax_description: tax.type,
tax_code: tax.name.parameterize(separator: '_'),
tax_name: tax.name,
tax_rate: tax_rate,
amount_currency: invoice.currency
)
invoice.applied_taxes << applied_tax

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

# NOTE: when applied on user current usage, the invoice is
# not created in DB
applied_tax.save! if invoice.persisted?

applied_taxes_amount_cents += tax_amount_cents
taxes_rate += pro_rated_taxes_rate(tax)

result.applied_taxes << applied_tax
end

invoice.taxes_amount_cents = applied_taxes_amount_cents.round
invoice.taxes_rate = taxes_rate.round(5)
result.invoice = invoice

result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

private

attr_reader :invoice, :provider_taxes

def applicable_taxes
return @applicable_taxes if defined? @applicable_taxes

output = {}
provider_taxes.fees.each do |fee_taxes|
fee_taxes.tax_breakdown.each do |tax|
key = calculate_key(tax)

next if output[key]

output[key] = tax
end
end

@applicable_taxes = output

@applicable_taxes
end

def indexed_fees
@indexed_fees ||= invoice.fees.each_with_object({}) do |fee, applied_taxes|
fee.applied_taxes.each do |applied_tax|
tax = OpenStruct.new(
name: applied_tax.tax_name,
rate: applied_tax.tax_rate,
type: applied_tax.tax_description
)
key = calculate_key(tax)

applied_taxes[key] ||= []
applied_taxes[key] << fee
end
end
end

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 }
end

def pro_rated_taxes_rate(tax)
tax_rate = tax.rate.is_a?(String) ? tax.rate.to_f * 100 : tax.rate

fees_rate = if invoice.sub_total_excluding_taxes_amount_cents.positive?
fees_amount_cents(tax).fdiv(invoice.sub_total_excluding_taxes_amount_cents)
else
# NOTE: when invoice have a 0 amount. The prorata is on the number of fees
key = calculate_key(tax)
indexed_fees[key].count.fdiv(invoice.fees.count)
end

fees_rate * tax_rate
end

def fees_amount_cents(tax)
key = calculate_key(tax)

indexed_fees[key].sum(&:sub_total_excluding_taxes_amount_cents)
end

def fetch_provider_taxes_result
Integrations::Aggregator::Taxes::Invoices::CreateService.call(invoice:)
end

def calculate_key(tax)
tax_rate = tax.rate.is_a?(String) ? tax.rate.to_f * 100 : tax.rate

"#{tax.type}-#{tax.name.parameterize(separator: "_")}-#{tax_rate}"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

class RemoveNullConstraintOnAppliedTaxes < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def change
change_column_null :fees_taxes, :tax_id, true
change_column_null :invoices_taxes, :tax_id, true

remove_index :fees_taxes, %i[fee_id tax_id]
remove_index :invoices_taxes, %i[invoice_id tax_id]

add_index :fees_taxes,
%i[fee_id tax_id],
unique: true,
where: "tax_id IS NOT NULL AND created_at >= '2023-09-12'",
algorithm: :concurrently

add_index :invoices_taxes,
%i[invoice_id tax_id],
unique: true,
where: "tax_id IS NOT NULL AND created_at >= '2023-09-12'",
algorithm: :concurrently
end
end
8 changes: 4 additions & 4 deletions db/schema.rb

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

58 changes: 58 additions & 0 deletions spec/services/fees/apply_provider_taxes_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Fees::ApplyProviderTaxesService, type: :service do
subject(:apply_service) { described_class.new(fee:, fee_taxes:) }

let(:customer) { create(:customer) }
let(:organization) { customer.organization }

let(:invoice) { create(:invoice, organization:, customer:) }

let(:fee) { create(:fee, invoice:, amount_cents: 1000, precise_coupons_amount_cents:) }
let(:precise_coupons_amount_cents) { 0 }

let(:fee_taxes) do
OpenStruct.new(
tax_breakdown: [
OpenStruct.new(name: 'tax 2', type: 'type2', rate: '0.12', tax_amount: 120),
OpenStruct.new(name: 'tax 3', type: 'type3', rate: '0.05', tax_amount: 50)
]
)
end

before do
fee_taxes
end

describe 'call' do
context 'when there is no applied taxes yet' do
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: 170, taxes_rate: 17)
end
end
end

context 'when fee already have taxes' do
before { create(:fee_applied_tax, fee:) }

it 'does not re-apply taxes' do
expect do
result = apply_service.call

expect(result).to be_success
end.not_to change { fee.applied_taxes.count }
end
end
end
end
Loading

0 comments on commit bdc6103

Please sign in to comment.