diff --git a/app/services/integrations/aggregator/taxes/invoices/payload.rb b/app/services/integrations/aggregator/taxes/invoices/payload.rb index 1e9db15bf9e..53b447ac5d5 100644 --- a/app/services/integrations/aggregator/taxes/invoices/payload.rb +++ b/app/services/integrations/aggregator/taxes/invoices/payload.rb @@ -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 diff --git a/app/services/invoices/customer_usage_service.rb b/app/services/invoices/customer_usage_service.rb index 47c7d5006ab..aedf78ba91e 100644 --- a/app/services/invoices/customer_usage_service.rb +++ b/app/services/invoices/customer_usage_service.rb @@ -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 @@ -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 @@ -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 diff --git a/spec/services/invoices/customer_usage_service_spec.rb b/spec/services/invoices/customer_usage_service_spec.rb index 750c69d63ec..5700ed9cd38 100644 --- a/spec/services/invoices/customer_usage_service_spec.rb +++ b/spec/services/invoices/customer_usage_service_spec.rb @@ -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) }