Skip to content

Commit

Permalink
feat(preview-invoice): add tax provider support for preview invoice (#…
Browse files Browse the repository at this point in the history
…3095)

## Context

Preview feature enables fetching first Lago invoice. This invoice is not
persisted and is calculated on the fly.

## Description

This PR adds tax provider logic for preview invoice
lovrocolic authored Jan 23, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent bda45af commit b74556d
Showing 3 changed files with 167 additions and 13 deletions.
11 changes: 5 additions & 6 deletions app/services/integrations/aggregator/taxes/base_service.rb
Original file line number Diff line number Diff line change
@@ -18,11 +18,10 @@ def integration
end

def integration_customer
@integration_customer ||=
customer
.integration_customers
.where(type: 'IntegrationCustomers::AnrokCustomer')
.first
@integration_customer ||= begin
int_customers = customer.integration_customers
int_customers.find { |ic| ic.type == 'IntegrationCustomers::AnrokCustomer' }
end
end

def headers
@@ -63,7 +62,7 @@ def process_response(body)
message = 'API limit' if message == 'Internal server error: resource contention.'

unless message.include?('API limit')
deliver_tax_error_webhook(customer:, code:, message:)
deliver_tax_error_webhook(customer:, code:, message:) if customer.persisted? # Do not send this webhook in preview mode
end

result.service_failure!(code:, message:)
76 changes: 70 additions & 6 deletions app/services/invoices/preview_service.rb
Original file line number Diff line number Diff line change
@@ -92,14 +92,12 @@ def compute_tax_and_totals

invoice.sub_total_excluding_taxes_amount_cents = invoice.fees_amount_cents - invoice.coupons_amount_cents

invoice.fees.each do |fee|
taxes_result = Fees::ApplyTaxesService.call(fee:)
taxes_result.raise_if_error!
if provider_taxation?
apply_provider_taxes
else
apply_taxes
end

taxes_result = Invoices::ApplyTaxesService.call(invoice:)
taxes_result.raise_if_error!

invoice.sub_total_including_taxes_amount_cents = (
invoice.sub_total_excluding_taxes_amount_cents + invoice.taxes_amount_cents
)
@@ -139,5 +137,71 @@ def wallet

@wallet = customer.wallets.active.first
end

def apply_taxes
invoice.fees.each do |fee|
taxes_result = Fees::ApplyTaxesService.call(fee:)
taxes_result.raise_if_error!
end

taxes_result = Invoices::ApplyTaxesService.call(invoice:)
taxes_result.raise_if_error!
end

def apply_provider_taxes
taxes_result = fetch_provider_taxes

if 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.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!
else
apply_zero_tax
end
rescue BaseService::ThrottlingError
apply_zero_tax
end

def provider_taxes_cache_key
[
"preview-taxes",
customer.id,
subscription.plan.updated_at.iso8601
].join("/")
end

def apply_zero_tax
invoice.taxes_amount_cents = 0
invoice.taxes_rate = 0
end

def fetch_provider_taxes
if customer.persisted?
taxes_result = Rails.cache.read(provider_taxes_cache_key)

unless taxes_result
taxes_result = Integrations::Aggregator::Taxes::Invoices::CreateDraftService.call(
invoice:,
fees: invoice.fees
)
Rails.cache.write(provider_taxes_cache_key, taxes_result, expires_in: 24.hours) if taxes_result.success?
end

taxes_result
else
Integrations::Aggregator::Taxes::Invoices::CreateDraftService.call(invoice:, fees: invoice.fees)
end
end

def provider_taxation?
customer.integration_customers&.find { |ic| ic.type == 'IntegrationCustomers::AnrokCustomer' }
end
end
end
93 changes: 92 additions & 1 deletion spec/services/invoices/preview_service_spec.rb
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

require 'rails_helper'

RSpec.describe Invoices::PreviewService, type: :service do
RSpec.describe Invoices::PreviewService, type: :service, cache: :memory do
subject(:preview_service) { described_class.new(customer:, subscription:) }

describe '#call' do
@@ -169,6 +169,97 @@
end
end
end

context 'with provider taxes' do
let(:integration) { create(:anrok_integration, organization:) }
let(:integration_customer) { build(: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')
File.read(p)
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
customer.integration_customers = [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)
allow_any_instance_of(Fee).to receive(:id).and_return('lago_fee_id') # rubocop:disable RSpec/AnyInstance
end

it 'creates preview invoice for 2 days' do
travel_to(timestamp) do
result = preview_service.call

expect(result).to be_success
expect(result.invoice.subscriptions.first).to eq(subscription)
expect(result.invoice.fees.length).to eq(1)
expect(result.invoice.invoice_type).to eq('subscription')
expect(result.invoice.issuing_date.to_s).to eq('2024-04-01')
expect(result.invoice.fees_amount_cents).to eq(6)
expect(result.invoice.sub_total_excluding_taxes_amount_cents).to eq(6)
expect(result.invoice.taxes_amount_cents).to eq(1) # 6 x 0.1
expect(result.invoice.sub_total_including_taxes_amount_cents).to eq(7)
expect(result.invoice.total_amount_cents).to eq(7)
end
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 'uses zero taxes' do
travel_to(timestamp) do
result = preview_service.call

expect(result).to be_success
expect(result.invoice.subscriptions.first).to eq(subscription)
expect(result.invoice.fees.length).to eq(1)
expect(result.invoice.invoice_type).to eq('subscription')
expect(result.invoice.issuing_date.to_s).to eq('2024-04-01')
expect(result.invoice.fees_amount_cents).to eq(6)
expect(result.invoice.sub_total_excluding_taxes_amount_cents).to eq(6)
expect(result.invoice.taxes_amount_cents).to eq(0)
expect(result.invoice.sub_total_including_taxes_amount_cents).to eq(6)
expect(result.invoice.total_amount_cents).to eq(6)
end
end
end

context 'with rails cache' do
let(:customer) { create(:customer, organization:) }

before { Rails.cache.clear }

it 'uses the Rails cache' do
travel_to(timestamp) do
key = [
'preview-taxes',
customer.id,
plan.updated_at.iso8601
].join('/')

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

context 'with anniversary billing' do

0 comments on commit b74556d

Please sign in to comment.