Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def fixed_charge? = false
def commitment? = false

def subscription? = false

def taxable? = sub_total_excluding_taxes_amount_cents.positive?
end

def invoice(customer, args)
Expand Down
1 change: 1 addition & 0 deletions app/services/fees/apply_provider_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def initialize(fee:, fee_taxes:)
def call
result.applied_taxes = []
return result if fee.applied_taxes.any?
return result if fee_taxes.nil?

applied_taxes_amount_cents = 0
applied_precise_taxes_amount_cents = 0.to_d
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ def initialize(invoice:, fees: nil)

delegate :customer, to: :invoice, allow_nil: true

# NOTE: Only fees with a positive amount can incur tax, so non-taxable fees are
# excluded from the provider request. This keeps the payload under the provider
# line-item limit (Anrok and Avalara reject payloads above 1200 items).
# Excluded fees are absent from the response and keep their zero taxes.
def taxable_fees
@taxable_fees ||= fees.select(&:taxable?)
end

# NOTE: With no taxable fee the request would carry an empty line-item array, which
# both providers reject. There is nothing to tax, so skip the provider and
# report no fee taxes.
def no_taxable_fees_result
result.fees = []
result
end

def process_void_response(body)
invoice_id = body["succeededInvoices"]&.first.try(:[], "id")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def action_path
def call
return result unless integration
return result unless ::Integrations::BaseIntegration::INTEGRATION_TAX_TYPES.include?(integration.type)
return no_taxable_fees_result if taxable_fees.empty?

throttle!(:anrok, :avalara)

Expand Down Expand Up @@ -44,7 +45,7 @@ def payload
invoice:,
customer:,
integration_customer:,
fees:
fees: taxable_fees
).body
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def action_path
def call
return result unless integration
return result unless ::Integrations::BaseIntegration::INTEGRATION_TAX_TYPES.include?(integration.type)
return no_taxable_fees_result if taxable_fees.empty?

throttle!(:anrok, :avalara)

Expand Down Expand Up @@ -46,7 +47,7 @@ def payload
invoice:,
customer:,
integration_customer:,
fees:
fees: taxable_fees
).body

invoice_data = payload_body.first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,86 @@
expect(breakdown2["enumedTaxCode"]).to eq("reverse_charge")
end

context "when a fee has no amount" do
let(:fees) do
[
{
addOnId: add_on_first.id,
unitAmountCents: 1200,
units: 2,
description: "desc-123",
invoiceDisplayName: "fee-123",
fromDatetime: current_time.utc.iso8601(3),
toDatetime: current_time.utc.iso8601(3)
},
{
addOnId: add_on_second.id,
unitAmountCents: 0,
units: 1,
description: "desc-12345",
invoiceDisplayName: "fee-12345",
fromDatetime: current_time.utc.iso8601(3),
toDatetime: current_time.utc.iso8601(3)
}
]
end

it "excludes it from the request" do
execute_graphql(
current_user: membership.user,
current_organization: organization,
permissions: required_permission,
query: mutation,
variables: {
input: {
customerId: customer.id,
currency:,
fees:
}
}
)

expect(lago_client).to have_received(:post_with_response) do |payload, _headers|
expect(payload.first["fees"].map { |fee| fee["item_id"] }).to eq([add_on_first.id])
end
end
end

context "when no fee has an amount" do
let(:fees) do
[
{
addOnId: add_on_first.id,
unitAmountCents: 0,
units: 2,
description: "desc-123",
invoiceDisplayName: "fee-123",
fromDatetime: current_time.utc.iso8601(3),
toDatetime: current_time.utc.iso8601(3)
}
]
end

it "returns no tax results without calling the provider" do
result = execute_graphql(
current_user: membership.user,
current_organization: organization,
permissions: required_permission,
query: mutation,
variables: {
input: {
customerId: customer.id,
currency:,
fees:
}
}
)

expect(result["data"]["fetchDraftInvoiceTaxes"]["collection"]).to eq([])
expect(lago_client).not_to have_received(:post_with_response)
end
end

context "when there is tax error" do
let(:body) do
path = Rails.root.join("spec/fixtures/integration_aggregator/taxes/invoices/failure_response.json")
Expand Down
13 changes: 13 additions & 0 deletions spec/services/fees/apply_provider_taxes_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@
end
end

context "when the fee was not reported to the tax provider" do
let(:fee_taxes) { nil }

it "leaves the fee untaxed" do
result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes).to be_empty
expect(fee.applied_taxes).to be_empty
expect(fee).to have_attributes(taxes_amount_cents: 0, taxes_rate: 0)
end
end

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,54 @@
expect(result.fees.first.tax_breakdown.last.rate).to eq("0.00")
end

context "when a fee has no amount" do
let(:fee_add_on_two) do
create(
:fee,
invoice:,
add_on: add_on_two,
amount_cents: 0,
created_at: current_time - 2.seconds
)
end
let(:params) { super().tap { |request_body| request_body.first["fees"] = [request_body.first["fees"].first] } }

it "excludes it from the request" do
service_call

expect(WebMock).to have_requested(:post, endpoint).with(body: params.to_json)
end
end

context "when no fee has an amount" do
let(:fee_add_on) do
create(
:fee,
invoice:,
add_on:,
amount_cents: 0,
created_at: current_time - 3.seconds
)
end
let(:fee_add_on_two) do
create(
:fee,
invoice:,
add_on: add_on_two,
amount_cents: 0,
created_at: current_time - 2.seconds
)
end

it "reports no fee taxes without calling the provider" do
result = service_call

expect(result).to be_success
expect(result.fees).to eq([])
expect(WebMock).not_to have_requested(:post, endpoint)
end
end

context "when special rules applied" do
let(:body) do
parsed_body = JSON.parse(base_body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,54 @@
it "does not create integration resource" do
expect { service_call }.not_to change { invoice.reload.integration_resources.count }
end

context "when a fee has no amount" do
let(:fee_add_on_two) do
create(
:fee,
invoice:,
add_on: add_on_two,
amount_cents: 0,
created_at: current_time - 2.seconds
)
end
let(:params) { super().tap { |body| body.first["fees"] = [body.first["fees"].first] } }

it "excludes it from the request" do
service_call

expect(WebMock).to have_requested(:post, endpoint).with(body: params.to_json)
end
end

context "when no fee has an amount" do
let(:fee_add_on) do
create(
:fee,
invoice:,
add_on:,
amount_cents: 0,
created_at: current_time - 3.seconds
)
end
let(:fee_add_on_two) do
create(
:fee,
invoice:,
add_on: add_on_two,
amount_cents: 0,
created_at: current_time - 2.seconds
)
end

it "reports no fee taxes without calling the provider" do
result = service_call

expect(result).to be_success
expect(result.fees).to eq([])
expect(WebMock).not_to have_requested(:post, endpoint)
end
end
end

context "when Avalara taxes are successfully fetched for finalized invoice" do
Expand Down Expand Up @@ -216,6 +264,25 @@
expect { service_call }.to change { invoice.reload.integration_resources.count }.by(1)
end

context "when a fee has no amount" do
let(:fee_add_on_two) do
create(
:fee,
invoice:,
add_on: add_on_two,
amount_cents: 0,
created_at: current_time - 2.seconds
)
end
let(:params) { super().tap { |body| body.first["fees"] = [body.first["fees"].first] } }

it "excludes it from the request" do
service_call

expect(WebMock).to have_requested(:post, endpoint).with(body: params.to_json)
end
end

context "when invoice is voided" do
let(:params) do
[
Expand Down
Loading
Loading