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
16 changes: 15 additions & 1 deletion app/services/fees/apply_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def call
def applicable_taxes
# organization.taxes - are all taxes created on the organization
return customer.organization.taxes.where(code: tax_codes) if tax_codes
return product_taxes if fee.product?
return fee.add_on.taxes if fee.add_on? && fee.add_on.taxes.any?
return fee.charge.taxes if fee.charge? && fee.charge.taxes.any?
return fee.fixed_charge.taxes if fee.fixed_charge? && fee.fixed_charge.taxes.any?
Expand All @@ -74,7 +75,20 @@ def applicable_taxes
return customer.taxes if customer.taxes.any?

# billing_entity.taxes - are the default taxes applied on the billing entity
Tax.joins(:billing_entities_taxes).where(billing_entities_taxes: {billing_entity_id: customer.billing_entity_id})
billing_entity_taxes(customer.billing_entity_id)
end

def product_taxes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not the scope of this PR but the previous applicable_taxes is a bit problematic already. We perform the ApplyTaxesService and accidentally does a N+1 https://github.com/getlago/lago-api/blob/main/app/services/invoices/compute_amounts_from_fees.rb#L20. I'd add a task in Linear to tackle this and avoid continue with the same issue for product_taxes.

rate_card_taxes = fee.rate_card_rate.rate_card.taxes
return rate_card_taxes if rate_card_taxes.any?
return plan.taxes if plan.taxes.any?
return customer.taxes if customer.taxes.any?

billing_entity_taxes(fee.billing_entity_id)
end

def billing_entity_taxes(billing_entity_id)
Tax.joins(:billing_entities_taxes).where(billing_entities_taxes: {billing_entity_id:})
end
end
end
15 changes: 14 additions & 1 deletion app/services/rate_cards/apply_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ def call
return result.not_found_failure!(resource: "tax") if (tax_codes - taxes_by_code.keys).present?

rate_card.with_lock do
rate_card.applied_taxes.where.not(tax_id: taxes_by_code.values.map(&:id)).destroy_all
current_tax_ids = rate_card.applied_taxes.pluck(:tax_id)
requested_tax_ids = taxes_by_code.values.map(&:id)

rate_card.applied_taxes.where.not(tax_id: requested_tax_ids).destroy_all

result.applied_taxes = tax_codes.map do |tax_code|
rate_card.applied_taxes
Expand All @@ -25,6 +28,8 @@ def call

rate_card.applied_taxes.reset
rate_card.taxes.reset

refresh_draft_invoices if current_tax_ids.sort != requested_tax_ids.sort
end

result
Expand All @@ -39,5 +44,13 @@ def call
def taxes_by_code
@taxes_by_code ||= rate_card.organization.taxes.where(code: tax_codes).index_by(&:code)
end

def refresh_draft_invoices

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question also for @rsempe @brunomiguelpinto do we need to apply the refresh for the rate_override as well?

invoice_ids = Fee.where(rate_card_rate_id: rate_card.rates.select(:id)).select(:invoice_id)

rate_card.organization.invoices.draft
.where(id: invoice_ids)
.update_all(ready_to_be_refreshed: true, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
end
end
end
23 changes: 23 additions & 0 deletions spec/factories/fees.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,29 @@
end
end

factory :product_fee, parent: :fee do
fee_type { "product" }
subscription do
association(
:subscription,
organization: invoice.organization,
customer: invoice.customer,
plan: association(:plan, organization: invoice.organization)
)
end
rate_card_rate do
association(
:rate_card_rate,
organization: invoice.organization,
rate_card: association(:rate_card, organization: invoice.organization)
)
end
invoiceable { rate_card_rate.rate_card.product }
billing_entity { invoice.billing_entity }
taxes_amount_cents { 0 }
taxes_precise_amount_cents { 0 }
end

factory :credit_fee, parent: :fee do
transient do
wallet_transaction { association(:wallet_transaction, organization:) }
Expand Down
160 changes: 160 additions & 0 deletions spec/services/fees/apply_taxes_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,166 @@
end
end

context "when fee is a product type" do
let(:plan) { create(:plan, organization:) }
let(:subscription) { create(:subscription, organization:, customer:, plan:) }
let(:product) { create(:product, organization:) }
let(:rate_card) { create(:rate_card, organization:, product:) }
let(:rate_card_rate) { create(:rate_card_rate, organization:, rate_card:) }
let(:rate_card_tax) { create(:tax, organization:, rate: 8) }
let(:fee) do
create(
:product_fee,
invoice:,
subscription:,
rate_card_rate:,
amount_cents: 1000,
precise_amount_cents: 1000.0
)
end

it "uses the rate card taxes before the other levels" do
create(:rate_card_applied_tax, rate_card:, tax: rate_card_tax, organization:)
create(:plan_applied_tax, plan:, tax: tax2)
create(:customer_applied_tax, customer:, tax: tax1)

result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes).to contain_exactly(
have_attributes(
fee:,
tax: rate_card_tax,
tax_description: rate_card_tax.description,
tax_code: rate_card_tax.code,
tax_name: rate_card_tax.name,
tax_rate: 8,
amount_currency: fee.currency,
amount_cents: 80,
precise_amount_cents: 80.0
)
)
expect(fee).to have_attributes(taxes_amount_cents: 80, taxes_precise_amount_cents: 80.0, taxes_rate: 8)
end

it "falls back to the plan taxes" do
create(:plan_applied_tax, plan:, tax: tax2)
create(:customer_applied_tax, customer:, tax: tax1)

result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(tax2.code)
end

it "falls back to the customer taxes" do
create(:customer_applied_tax, customer:, tax: tax1)

result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(tax1.code)
end

it "falls back to the billing entity taxes" do
result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(tax3.code)
end

context "when the subscription uses another billing entity" do
let(:subscription_billing_entity) { create(:billing_entity, organization:) }
let(:subscription_tax) { create(:tax, organization:, rate: 20) }
let(:invoice) { create(:invoice, organization:, customer:, billing_entity: subscription_billing_entity) }
let(:subscription) do
create(:subscription, organization:, customer:, plan:, billing_entity: subscription_billing_entity)
end

before do
create(
:billing_entity_applied_tax,
billing_entity: subscription_billing_entity,
tax: subscription_tax
)
end

it "falls back to the subscription billing entity taxes" do
result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(subscription_tax.code)
end
end

it "applies no taxes when every level is empty" do
billing_entity.applied_taxes.destroy_all

result = apply_service.call

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

it "uses every assigned rate card tax" do
create(:rate_card_applied_tax, rate_card:, tax: rate_card_tax, organization:)
create(:rate_card_applied_tax, rate_card:, tax: tax1, organization:)
create(:plan_applied_tax, plan:, tax: tax2)

result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(rate_card_tax.code, tax1.code)
expect(fee).to have_attributes(taxes_amount_cents: 180, taxes_precise_amount_cents: 180.0, taxes_rate: 18)
end

it "treats a zero-percent rate card tax as an override" do
zero_tax = create(:tax, organization:, rate: 0)
create(:rate_card_applied_tax, rate_card:, tax: zero_tax, organization:)
create(:plan_applied_tax, plan:, tax: tax2)

result = apply_service.call

expect(result).to be_success
expect(result.applied_taxes.map(&:tax_code)).to contain_exactly(zero_tax.code)
expect(fee).to have_attributes(taxes_amount_cents: 0, taxes_precise_amount_cents: 0.0, taxes_rate: 0)
end

it "uses the next configured level for a replacement fee after removing the rate card override" do
create(:rate_card_applied_tax, rate_card:, tax: rate_card_tax, organization:)
create(:plan_applied_tax, plan:, tax: tax2)

first_result = apply_service.call
RateCards::ApplyTaxesService.call!(rate_card:, tax_codes: [])

replacement_fee = create(:product_fee, invoice:, subscription:, rate_card_rate:, amount_cents: 1000)
replacement_result = described_class.call(fee: replacement_fee)

expect(first_result.applied_taxes.map(&:tax_code)).to contain_exactly(rate_card_tax.code)
expect(replacement_result.applied_taxes.map(&:tax_code)).to contain_exactly(tax2.code)
end

it "keeps the stored tax snapshot after the invoice is finalized" do
create(:rate_card_applied_tax, rate_card:, tax: rate_card_tax, organization:)
apply_service.call
invoice.update!(status: :finalized)

RateCards::ApplyTaxesService.call!(rate_card:, tax_codes: [])

expect(fee.reload.applied_taxes).to contain_exactly(
have_attributes(
tax_id: rate_card_tax.id,
tax_code: rate_card_tax.code,
tax_name: rate_card_tax.name,
tax_rate: 8,
amount_cents: 80,
precise_amount_cents: 80.0
)
)
end
end

context "when fee is a fixed charge type with taxes" do
let(:fixed_charge) { create(:fixed_charge, organization:) }
let(:fee) { create(:fixed_charge_fee, invoice:, amount_cents: 1000, precise_amount_cents: 1000.0, fixed_charge:) }
Expand Down
28 changes: 28 additions & 0 deletions spec/services/invoices/compute_amounts_from_fees_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@
expect(fee2.taxes_amount_cents).to eq(84) # (379 - 100) * (10 + 20) / 100
end

context "when the invoice contains a product fee" do
let(:plan) { create(:plan, organization:) }
let(:subscription) { create(:subscription, organization:, customer:, plan:) }
let(:rate_card) { create(:rate_card, organization:) }
let(:rate_card_rate) { create(:rate_card_rate, organization:, rate_card:) }
let(:fee1) do
create(
:product_fee,
invoice:,
subscription:,
rate_card_rate:,
amount_cents: 151,
precise_amount_cents: 151
)
end

before do
create(:rate_card_applied_tax, rate_card:, tax: tax1, organization:)
end

it "applies the rate card taxes through the invoice calculation entry point" do
compute_amounts.call

expect(fee1.reload.applied_taxes.map(&:tax_code)).to contain_exactly(tax1.code)
expect(fee1).to have_attributes(taxes_rate: 10, taxes_amount_cents: 15)
end
end

it "sets fees_amount_cents from the list of fees" do
expect { compute_amounts.call }.to change(invoice, :fees_amount_cents).from(0).to(530)
end
Expand Down
50 changes: 50 additions & 0 deletions spec/services/rate_cards/apply_taxes_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,56 @@
expect(rate_card).to have_received(:with_lock)
end

context "when the rate card is used by an invoice fee" do
let(:customer) { create(:customer, organization:) }
let(:plan) { create(:plan, organization:) }
let(:subscription) { create(:subscription, organization:, customer:, plan:) }
let(:invoice) { create(:invoice, organization:, customer:, status: invoice_status) }
let(:invoice_status) { :draft }
let(:rate_card_rate) { create(:rate_card_rate, organization:, rate_card:) }

before do
create(:product_fee, invoice:, subscription:, rate_card_rate:)
end

it "marks a draft invoice for refresh and updates its timestamp when the assignments change" do
invoice.update!(updated_at: 1.day.ago)
previous_updated_at = invoice.updated_at

result

expect(invoice.reload.ready_to_be_refreshed).to be(true)
expect(invoice.updated_at).to be > previous_updated_at
end

it "does not mark a draft invoice when the assignments do not change" do
create(:rate_card_applied_tax, rate_card:, tax: tax1, organization:)
create(:rate_card_applied_tax, rate_card:, tax: tax2, organization:)

expect { result }.not_to change { invoice.reload.ready_to_be_refreshed }
end

context "when every assignment is removed" do
let(:tax_codes) { [] }

before do
create(:rate_card_applied_tax, rate_card:, tax: tax1, organization:)
end

it "marks a draft invoice for refresh" do
expect { result }.to change { invoice.reload.ready_to_be_refreshed }.from(false).to(true)
end
end

context "when the invoice is finalized" do
let(:invoice_status) { :finalized }

it "does not mark the invoice for refresh" do
expect { result }.not_to change { invoice.reload.ready_to_be_refreshed }
end
end
end

context "when tax codes contain duplicates" do
let(:tax_codes) { [tax1.code, tax1.code] }

Expand Down
Loading