Skip to content

Commit

Permalink
feat (tax-integrations): handle current usage with tax provider (#2468)
Browse files Browse the repository at this point in the history
## Context

Currently Lago is implementing integration with tax provider Anrok

## Description

This PR adds logic to use tax provider taxes when fetching current usage
  • Loading branch information
lovrocolic authored Aug 29, 2024
1 parent e61f10c commit 61a49ff
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize(integration:, customer:, invoice:, integration_customer:, fees: [
@customer = customer
@integration_customer = integration_customer
@invoice = invoice
@fees = fees.is_a?(Array) ? fees : fees.order(created_at: :asc)
@fees = (fees.is_a?(Array) || !fees&.first&.persisted?) ? fees : fees.order(created_at: :asc)
end

def body
Expand Down
49 changes: 48 additions & 1 deletion app/services/invoices/customer_usage_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ def compute_usage
)

add_charge_fees
compute_amounts

if customer_provider_taxation?
compute_amounts_with_provider_taxes
else
compute_amounts
end

format_usage
end
Expand Down Expand Up @@ -136,10 +141,48 @@ def compute_amounts
invoice.total_amount_cents = invoice.fees_amount_cents + invoice.taxes_amount_cents
end

def compute_amounts_with_provider_taxes
invoice.fees_amount_cents = invoice.fees.sum(&:amount_cents)

taxes_result = Rails.cache.read(provider_taxes_cache_key)

unless taxes_result
# Call the service if the cache is empty
taxes_result = Integrations::Aggregator::Taxes::Invoices::CreateDraftService.call(invoice:, fees: invoice.fees)

# Cache the result only if it's successful
Rails.cache.write(provider_taxes_cache_key, taxes_result, expires_in: 24.hours) if taxes_result.success?
end

return result.validation_failure!(errors: {tax_error: [taxes_result.error.code]}) unless taxes_result.success?

result.fees_taxes = taxes_result.fees

invoice.fees.each do |fee|
fee_taxes = result.fees_taxes.find { |item| item.item_id == fee.item_id }

res = Fees::ApplyProviderTaxesService.call(fee:, fee_taxes:)
res.raise_if_error!
end

res = Invoices::ApplyProviderTaxesService.call(invoice:, provider_taxes: result.fees_taxes)
res.raise_if_error!

invoice.total_amount_cents = invoice.fees_amount_cents + invoice.taxes_amount_cents
end

def charge_cache_key(charge)
Subscriptions::ChargeCacheService.new(subscription:, charge:).cache_key
end

def provider_taxes_cache_key
[
'provider-taxes',
subscription.id,
plan.updated_at.iso8601
].join('/')
end

def charge_cache_expiration
(boundaries[:charges_to_datetime] - Time.current).to_i.seconds
end
Expand All @@ -156,5 +199,9 @@ def format_usage
fees: invoice.fees
)
end

def customer_provider_taxation?
@customer_provider_taxation ||= invoice.customer.anrok_customer
end
end
end
85 changes: 85 additions & 0 deletions spec/services/invoices/customer_usage_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,91 @@
end
end

context 'when there is tax provider integration' do
let(:integration) { create(:anrok_integration, organization:) }
let(:integration_customer) { create(:anrok_customer, integration:, customer:) }
let(:response) { instance_double(Net::HTTPOK) }
let(:lago_client) { instance_double(LagoHttpClient::Client) }
let(:endpoint) { 'https://api.nango.dev/v1/anrok/draft_invoices' }
let(:body) do
p = Rails.root.join('spec/fixtures/integration_aggregator/taxes/invoices/success_response.json')
json = File.read(p)

# setting item_id based on the test example
response = JSON.parse(json)
response['succeededInvoices'].first['fees'].last['item_id'] = charge.billable_metric.id

response.to_json
end
let(:integration_collection_mapping) do
create(
:netsuite_collection_mapping,
integration:,
mapping_type: :fallback_item,
settings: {external_id: '1', external_account_code: '11', external_name: ''}
)
end

before do
integration_collection_mapping
integration_customer

allow(LagoHttpClient::Client).to receive(:new).with(endpoint).and_return(lago_client)
allow(lago_client).to receive(:post_with_response).and_return(response)
allow(response).to receive(:body).and_return(body)
end

it 'initializes an invoice' do
result = usage_service.call

aggregate_failures do
expect(result).to be_success
expect(result.invoice).to be_a(Invoice)

expect(result.usage).to have_attributes(
from_datetime: Time.current.beginning_of_month.iso8601,
to_datetime: Time.current.end_of_month.iso8601,
issuing_date: Time.zone.today.end_of_month.iso8601,
currency: 'EUR',
amount_cents: 2532, # 1266 * 2,
taxes_amount_cents: 253, # 2532 * 0.1
total_amount_cents: 2785
)
expect(result.usage.fees.size).to eq(1)
expect(result.usage.fees.first.charge.invoice_display_name).to eq(charge.invoice_display_name)
end
end

it 'uses the Rails cache' do
key = [
'provider-taxes',
subscription.id,
plan.updated_at.iso8601
].join('/')

expect do
usage_service.call
end.to change { Rails.cache.exist?(key) }.from(false).to(true)
end

context 'when there is error received from the provider' do
let(:body) do
p = Rails.root.join('spec/fixtures/integration_aggregator/taxes/invoices/failure_response.json')
File.read(p)
end

it 'returns tax error' do
result = usage_service.call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
expect(result.error.messages[:tax_error]).to eq(['taxDateTooFarInFuture'])
end
end
end
end

context 'with subscription started in current billing period' do
before { subscription.update!(started_at: Time.zone.today) }

Expand Down

0 comments on commit 61a49ff

Please sign in to comment.