From 5ddaa477b51e4637e0e7eec432ab8ec011a79b5b Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 30 Sep 2024 17:00:38 +0200 Subject: [PATCH 1/9] wip adjusting tax precise amount --- app/models/credit_note.rb | 12 ++++++++ app/models/invoice.rb | 4 ++- app/services/credit_notes/create_service.rb | 15 ++++++++++ app/services/credit_notes/estimate_service.rb | 30 +++++++++++++++++-- .../credit_notes/validate_item_service.rb | 12 +++++--- app/services/credit_notes/validate_service.rb | 6 ++++ 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/app/models/credit_note.rb b/app/models/credit_note.rb index ce08d9edba7..07bc84b790c 100644 --- a/app/models/credit_note.rb +++ b/app/models/credit_note.rb @@ -129,6 +129,18 @@ def sub_total_excluding_taxes_amount_cents end alias_method :sub_total_excluding_taxes_amount_currency, :currency + def precise_total + items.sum(&:precise_amount_cents) + precise_taxes_amount_cents + end + + def taxes_rounding_adjustment + taxes_amount_cents - precise_taxes_amount_cents + end + + def rounding_adjustment + total_amount_cents - precise_total + end + private def ensure_number diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 911282d2dc6..a1cd503f8a2 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -272,7 +272,9 @@ def creditable_amount_cents fee_rate = fee.creditable_amount_cents.fdiv(fees_total_creditable) prorated_credit_amount = credit_adjustement * fee_rate (fee.creditable_amount_cents - prorated_credit_amount) * (fee.taxes_rate || 0) - end.fdiv(100).round + end.fdiv(100) + # end.fdiv(100).round + # BECAUE OF THIS ROUND the returned value is wrong... fees_total_creditable - credit_adjustement + vat end diff --git a/app/services/credit_notes/create_service.rb b/app/services/credit_notes/create_service.rb index 410d4a0da4d..9000367b970 100644 --- a/app/services/credit_notes/create_service.rb +++ b/app/services/credit_notes/create_service.rb @@ -204,10 +204,25 @@ def compute_amounts_and_taxes credit_note.precise_coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents credit_note.coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents.round credit_note.precise_taxes_amount_cents = taxes_result.taxes_amount_cents + adjust_credit_note_tax_rounding if credit_note_for_all_remaining_amount? + credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round credit_note.taxes_rate = taxes_result.taxes_rate taxes_result.applied_taxes.each { |applied_tax| credit_note.applied_taxes << applied_tax } end + + def credit_note_for_all_remaining_amount? + credit_note.items.sum(&:precise_amount_cents) == credit_note.invoice.fees.sum(&:creditable_amount_cents) + end + + def adjust_credit_note_tax_rounding + credit_note.precise_taxes_amount_cents -= all_rounding_tax_adjustments + credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round + end + + def all_rounding_tax_adjustments + credit_note.invoice.credit_notes.sum(&:taxes_rounding_adjustment) + end end end diff --git a/app/services/credit_notes/estimate_service.rb b/app/services/credit_notes/estimate_service.rb index cae724c8496..c512b6b2c45 100644 --- a/app/services/credit_notes/estimate_service.rb +++ b/app/services/credit_notes/estimate_service.rb @@ -23,10 +23,14 @@ def call balance_amount_currency: invoice.currency ) + # byebug validate_items return result unless result.success? compute_amounts_and_taxes + valid_credit_note? + # byebug + return result unless result.success? result.credit_note = credit_note result @@ -58,7 +62,12 @@ def validate_items end end + def valid_credit_note? + CreditNotes::ValidateService.new(result, item: credit_note).valid? + end + def valid_item?(item) + # byebug CreditNotes::ValidateItemService.new(result, item:).valid? end @@ -71,7 +80,9 @@ def compute_amounts_and_taxes credit_note.precise_coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents credit_note.coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents.round credit_note.precise_taxes_amount_cents = taxes_result.taxes_amount_cents - credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round + # adjust_credit_note_tax_precise_rounding(taxes_result) if credit_note_for_all_remaining_amount? + + credit_note.taxes_amount_cents = credit_note.precise_taxes_amount_cents.round credit_note.taxes_rate = taxes_result.taxes_rate taxes_result.applied_taxes.each { |applied_tax| credit_note.applied_taxes << applied_tax } @@ -79,16 +90,31 @@ def compute_amounts_and_taxes credit_note.credit_amount_cents = ( credit_note.items.sum(&:amount_cents) - taxes_result.coupons_adjustment_amount_cents + - taxes_result.taxes_amount_cents + credit_note.precise_taxes_amount_cents ).round compute_refundable_amount credit_note.total_amount_cents = credit_note.credit_amount_cents end + def credit_note_for_all_remaining_amount? + credit_note.items.sum(&:precise_amount_cents) == credit_note.invoice.fees.sum(&:creditable_amount_cents) + end + + def adjust_credit_note_tax_precise_rounding(taxes_result) + credit_note.precise_taxes_amount_cents -= all_rounding_tax_adjustments + credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round + end + + def all_rounding_tax_adjustments + credit_note.invoice.credit_notes.sum(&:taxes_rounding_adjustment) + end + def compute_refundable_amount credit_note.refund_amount_cents = credit_note.credit_amount_cents + # byebug + # invoice.refundable_amount_cents is incorrect - in our case it returns 82.00 refundable_amount_cents = invoice.refundable_amount_cents return unless credit_note.credit_amount_cents > refundable_amount_cents diff --git a/app/services/credit_notes/validate_item_service.rb b/app/services/credit_notes/validate_item_service.rb index 11895643cff..7c73d6b44b0 100644 --- a/app/services/credit_notes/validate_item_service.rb +++ b/app/services/credit_notes/validate_item_service.rb @@ -7,7 +7,7 @@ def valid? valid_item_amount? valid_individual_amount? - valid_global_amount? + # valid_global_amount? if errors? result.validation_failure!(errors:) @@ -42,9 +42,13 @@ def invoice_credit_note_total_amount_cents credited_invoice_amount_cents + refunded_invoice_amount_cents end - def total_item_amount_cents - (item.amount_cents + (item.amount_cents * fee.taxes_rate).fdiv(100)).round - end + # QUESTION HERE: + # we don't save taxes on the credit_note item. does it make sense then to check taxes on credit note level rather + # than on item level? + # + # def total_item_amount_cents + # (item.amount_cents + (item.amount_cents * fee.taxes_rate).fdiv(100)).round + # end def valid_fee? return true if item.fee.present? diff --git a/app/services/credit_notes/validate_service.rb b/app/services/credit_notes/validate_service.rb index a6952c4f914..dc00849565a 100644 --- a/app/services/credit_notes/validate_service.rb +++ b/app/services/credit_notes/validate_service.rb @@ -3,11 +3,17 @@ module CreditNotes class ValidateService < BaseValidator def valid? + # byebug valid_invoice_status? +# byebug valid_items_amount? +# byebug valid_refund_amount? +# byebug valid_credit_amount? +# byebug valid_global_amount? +# byebug if errors? result.validation_failure!(errors:) From 3ef04e2258565e85245114ed83c67d05670b26f0 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 1 Oct 2024 09:27:26 +0200 Subject: [PATCH 2/9] more changes to make adjustment in the taxes --- app/models/invoice.rb | 2 +- app/services/credit_notes/create_service.rb | 5 ++--- app/services/credit_notes/estimate_service.rb | 9 +++------ app/services/credit_notes/validate_service.rb | 6 ------ 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index a1cd503f8a2..b80fe63c903 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -274,7 +274,7 @@ def creditable_amount_cents (fee.creditable_amount_cents - prorated_credit_amount) * (fee.taxes_rate || 0) end.fdiv(100) # end.fdiv(100).round - # BECAUE OF THIS ROUND the returned value is wrong... + # BECAUE OF THIS ROUND the returned value is not precise fees_total_creditable - credit_adjustement + vat end diff --git a/app/services/credit_notes/create_service.rb b/app/services/credit_notes/create_service.rb index 9000367b970..dd13acb7df2 100644 --- a/app/services/credit_notes/create_service.rb +++ b/app/services/credit_notes/create_service.rb @@ -206,19 +206,18 @@ def compute_amounts_and_taxes credit_note.precise_taxes_amount_cents = taxes_result.taxes_amount_cents adjust_credit_note_tax_rounding if credit_note_for_all_remaining_amount? - credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round + credit_note.taxes_amount_cents = credit_note.precise_taxes_amount_cents.round credit_note.taxes_rate = taxes_result.taxes_rate taxes_result.applied_taxes.each { |applied_tax| credit_note.applied_taxes << applied_tax } end def credit_note_for_all_remaining_amount? - credit_note.items.sum(&:precise_amount_cents) == credit_note.invoice.fees.sum(&:creditable_amount_cents) + credit_note.invoice.creditable_amount_cents == 0 end def adjust_credit_note_tax_rounding credit_note.precise_taxes_amount_cents -= all_rounding_tax_adjustments - credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round end def all_rounding_tax_adjustments diff --git a/app/services/credit_notes/estimate_service.rb b/app/services/credit_notes/estimate_service.rb index c512b6b2c45..47e8417d068 100644 --- a/app/services/credit_notes/estimate_service.rb +++ b/app/services/credit_notes/estimate_service.rb @@ -67,7 +67,6 @@ def valid_credit_note? end def valid_item?(item) - # byebug CreditNotes::ValidateItemService.new(result, item:).valid? end @@ -80,7 +79,7 @@ def compute_amounts_and_taxes credit_note.precise_coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents credit_note.coupons_adjustment_amount_cents = taxes_result.coupons_adjustment_amount_cents.round credit_note.precise_taxes_amount_cents = taxes_result.taxes_amount_cents - # adjust_credit_note_tax_precise_rounding(taxes_result) if credit_note_for_all_remaining_amount? + adjust_credit_note_tax_precise_rounding if credit_note_for_all_remaining_amount? credit_note.taxes_amount_cents = credit_note.precise_taxes_amount_cents.round credit_note.taxes_rate = taxes_result.taxes_rate @@ -101,9 +100,8 @@ def credit_note_for_all_remaining_amount? credit_note.items.sum(&:precise_amount_cents) == credit_note.invoice.fees.sum(&:creditable_amount_cents) end - def adjust_credit_note_tax_precise_rounding(taxes_result) + def adjust_credit_note_tax_precise_rounding credit_note.precise_taxes_amount_cents -= all_rounding_tax_adjustments - credit_note.taxes_amount_cents = taxes_result.taxes_amount_cents.round end def all_rounding_tax_adjustments @@ -113,8 +111,7 @@ def all_rounding_tax_adjustments def compute_refundable_amount credit_note.refund_amount_cents = credit_note.credit_amount_cents - # byebug - # invoice.refundable_amount_cents is incorrect - in our case it returns 82.00 + # invoice.refundable_amount_cents is incorrect - in our case it returns 8200, but it should be 8199... refundable_amount_cents = invoice.refundable_amount_cents return unless credit_note.credit_amount_cents > refundable_amount_cents diff --git a/app/services/credit_notes/validate_service.rb b/app/services/credit_notes/validate_service.rb index dc00849565a..a6952c4f914 100644 --- a/app/services/credit_notes/validate_service.rb +++ b/app/services/credit_notes/validate_service.rb @@ -3,17 +3,11 @@ module CreditNotes class ValidateService < BaseValidator def valid? - # byebug valid_invoice_status? -# byebug valid_items_amount? -# byebug valid_refund_amount? -# byebug valid_credit_amount? -# byebug valid_global_amount? -# byebug if errors? result.validation_failure!(errors:) From a2c7b3d0e0d1cc4570ffeaf3c8200f3e749f0181 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 1 Oct 2024 11:13:39 +0200 Subject: [PATCH 3/9] some cleanup --- app/services/credit_notes/estimate_service.rb | 4 ---- app/services/credit_notes/validate_item_service.rb | 12 +++++------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/services/credit_notes/estimate_service.rb b/app/services/credit_notes/estimate_service.rb index 47e8417d068..e655ec381c3 100644 --- a/app/services/credit_notes/estimate_service.rb +++ b/app/services/credit_notes/estimate_service.rb @@ -23,14 +23,10 @@ def call balance_amount_currency: invoice.currency ) - # byebug validate_items return result unless result.success? compute_amounts_and_taxes - valid_credit_note? - # byebug - return result unless result.success? result.credit_note = credit_note result diff --git a/app/services/credit_notes/validate_item_service.rb b/app/services/credit_notes/validate_item_service.rb index 7c73d6b44b0..3bf2af3f93e 100644 --- a/app/services/credit_notes/validate_item_service.rb +++ b/app/services/credit_notes/validate_item_service.rb @@ -7,6 +7,8 @@ def valid? valid_item_amount? valid_individual_amount? + # we don't save taxes on the credit_note item and we'll adjust them when creating credit notes, so it makes sense + # to check taxes on credit note level, where taxes are calculated and stored. # valid_global_amount? if errors? @@ -42,13 +44,9 @@ def invoice_credit_note_total_amount_cents credited_invoice_amount_cents + refunded_invoice_amount_cents end - # QUESTION HERE: - # we don't save taxes on the credit_note item. does it make sense then to check taxes on credit note level rather - # than on item level? - # - # def total_item_amount_cents - # (item.amount_cents + (item.amount_cents * fee.taxes_rate).fdiv(100)).round - # end + def total_item_amount_cents + (item.amount_cents + (item.amount_cents * fee.taxes_rate).fdiv(100)).round + end def valid_fee? return true if item.fee.present? From b6880cedd24ac74e0988b3dd92e8ef7a0a100e28 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 1 Oct 2024 12:00:00 +0200 Subject: [PATCH 4/9] remove existing test fot the check we wont do --- app/models/invoice.rb | 4 +-- .../validate_item_service_spec.rb | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index b80fe63c903..e09a47dddab 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -272,8 +272,8 @@ def creditable_amount_cents fee_rate = fee.creditable_amount_cents.fdiv(fees_total_creditable) prorated_credit_amount = credit_adjustement * fee_rate (fee.creditable_amount_cents - prorated_credit_amount) * (fee.taxes_rate || 0) - end.fdiv(100) - # end.fdiv(100).round + end.fdiv(100).round + # end.fdiv(100) # BECAUE OF THIS ROUND the returned value is not precise fees_total_creditable - credit_adjustement + vat diff --git a/spec/services/credit_notes/validate_item_service_spec.rb b/spec/services/credit_notes/validate_item_service_spec.rb index 7aea3019729..3c2eed93a9a 100644 --- a/spec/services/credit_notes/validate_item_service_spec.rb +++ b/spec/services/credit_notes/validate_item_service_spec.rb @@ -96,19 +96,20 @@ end end - context 'when reaching invoice creditable amount' do - before do - create(:credit_note, invoice:, total_amount_cents: 99) - end - - it 'fails the validation' do - aggregate_failures do - expect(validator).not_to be_valid - - expect(result.error).to be_a(BaseService::ValidationFailure) - expect(result.error.messages[:amount_cents]).to eq(['higher_than_remaining_invoice_amount']) - end - end - end + # this check should not be on the item level, as taxes are applied and saved on the credit note level + # context 'when reaching invoice creditable amount' do + # before do + # create(:credit_note, invoice:, total_amount_cents: 99) + # end + # + # it 'fails the validation' do + # aggregate_failures do + # expect(validator).not_to be_valid + # + # expect(result.error).to be_a(BaseService::ValidationFailure) + # expect(result.error.messages[:amount_cents]).to eq(['higher_than_remaining_invoice_amount']) + # end + # end + # end end end From 5c16c1eb85dd1ccd919cd4dcbb1138aed8779909 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 4 Oct 2024 10:15:36 +0200 Subject: [PATCH 5/9] add almost the crasiest test fof credit note --- app/models/invoice.rb | 2 +- spec/models/credit_note_spec.rb | 66 +++++- spec/scenarios/credit_note_spec.rb | 332 +++++++++++++++++++++++++++++ spec/support/scenarios_helper.rb | 22 ++ 4 files changed, 419 insertions(+), 3 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index e09a47dddab..15df51514c2 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -274,7 +274,7 @@ def creditable_amount_cents (fee.creditable_amount_cents - prorated_credit_amount) * (fee.taxes_rate || 0) end.fdiv(100).round # end.fdiv(100) - # BECAUE OF THIS ROUND the returned value is not precise + # BECAUSE OF THIS ROUND the returned value is not precise fees_total_creditable - credit_adjustement + vat end diff --git a/spec/models/credit_note_spec.rb b/spec/models/credit_note_spec.rb index 9ad7395c2e7..4b7abdfe618 100644 --- a/spec/models/credit_note_spec.rb +++ b/spec/models/credit_note_spec.rb @@ -3,7 +3,17 @@ require 'rails_helper' RSpec.describe CreditNote, type: :model do - subject(:credit_note) { create(:credit_note) } + subject(:credit_note) do + create :credit_note, credit_amount_cents: 11000, total_amount_cents: 11000, taxes_amount_cents: 1000, + taxes_rate: 10.0, precise_taxes_amount_cents: 1000 + end + + let(:item) { create(:credit_note_item, credit_note:, precise_amount_cents: 10000, amount_cents: 1000) } + + before do + item + credit_note.reload + end it_behaves_like 'paper_trail traceable' @@ -243,13 +253,31 @@ end end - describe ' #sub_total_excluding_taxes_amount_cents' do + describe '#sub_total_excluding_taxes_amount_cents' do it 'returs the total amount without the taxes' do expect(credit_note.sub_total_excluding_taxes_amount_cents) .to eq(credit_note.items.sum(&:precise_amount_cents) - credit_note.precise_coupons_adjustment_amount_cents) end end + describe '#precise_total' do + it 'returns the total precise amount including precise taxes' do + expect(credit_note.precise_total).to eq(11000) + end + end + + describe '#taxes_rounding_adjustment' do + it 'returns the difference between taxes and precise taxes' do + expect(credit_note.taxes_rounding_adjustment).to eq(0) + end + end + + describe '#rounding_adjustment' do + it 'returns the difference between credit note total and credit note precise total' do + expect(credit_note.taxes_rounding_adjustment).to eq(0) + end + end + describe '#should_sync_credit_note?' do subject(:method_call) { credit_note.should_sync_credit_note? } @@ -328,4 +356,38 @@ end end end + + + context 'when taxes are not precise' do + subject(:credit_note) do + create :credit_note, credit_amount_cents: 8200, total_amount_cents: 8200, taxes_amount_cents: 1367, + taxes_rate: 20.0, precise_taxes_amount_cents: 1366.6 + end + + let(:item) { create(:credit_note_item, credit_note:, precise_amount_cents: 6833, amount_cents: 6833) } + + before do + item + end + + describe '#precise_total' do + it 'returns the total precise amount including precise taxes' do + expect(credit_note.precise_total).to eq(8199.6) + end + end + + describe '#taxes_rounding_adjustment' do + it 'returns the difference between taxes and precise taxes' do + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + end + end + + describe '#rounding_adjustment' do + it 'returns the difference between credit note total and credit note precise total' do + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + end + end + end end + + diff --git a/spec/scenarios/credit_note_spec.rb b/spec/scenarios/credit_note_spec.rb index 4c8d51fe027..529e88241f7 100644 --- a/spec/scenarios/credit_note_spec.rb +++ b/spec/scenarios/credit_note_spec.rb @@ -336,4 +336,336 @@ end end end + + context 'when creating credit notes for small items with taxes, so sum of items with their taxes is bigger than invoice total amount' do + let(:tax) { create(:tax, organization:, rate: 20) } + + context 'two similar items are refunded separately' do + let(:add_ons) do + [ create(:add_on, organization:, amount_cents: 68_33), + create(:add_on, organization:, amount_cents: 68_33)] + end + + it 'solves the rounding issue' do + # create a one off invoice with two addons and small amounts as feed + create_one_off_invoice(customer, add_ons) + # invoice amount should be with taxes calculated on items sum: + invoice = customer.invoices.order(:created_at).last + expect(invoice.total_amount_cents).to eq(163_99) + expect(invoice.taxes_amount_cents).to eq(27_33) + fees = invoice.fees + invoice.update(payment_status: 'succeeded') + + # estimate and create credit notes for first item - full refund; the taxes are rounded to higher number + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[0].id, + amount_cents: 68_33 + } + ] + ) + + # Estimate the credit notes amount on one fee rounds the taxes to higher number + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: "1366.6", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8200, + max_refundable_amount_cents: 8200, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 82_00, + items: [ + { + fee_id: fees[0].id, + amount_cents: 68_33 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 82_00, + total_amount_cents: 82_00, + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: 1366.6, + ) + expect(credit_note.precise_total).to eq(8199.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + + #when issuing second credit note, it should be rounded to lower number + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[1].id, + amount_cents: 68_33 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_66, + precise_taxes_amount_cents: "1366.2", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8199, + max_refundable_amount_cents: 8199, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 81_99, + items: [ + { + fee_id: fees[1].id, + amount_cents: 68_33 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 8199, + total_amount_cents: 8199, + taxes_amount_cents: 13_66, + precise_taxes_amount_cents: 1366.2, + ) + expect(credit_note.precise_total).to eq(8199.2) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) + end + + end + + context 'four items are refunded separately, some whole, some in parts' do + let(:add_ons) do + [ create(:add_on, organization:, amount_cents: 68_33), + create(:add_on, organization:, amount_cents: 68_33), + create(:add_on, organization:, amount_cents: 68_33), + create(:add_on, organization:, amount_cents: 68_33)] + end + + + it 'solves the rounding issue' do + # create a one off invoice with two addons and small amounts as feed + create_one_off_invoice(customer, add_ons) + # invoice amount should be with taxes calculated on items sum: + invoice = customer.invoices.order(:created_at).last + expect(invoice.total_amount_cents).to eq(327_98) + expect(invoice.taxes_amount_cents).to eq(54_66) + fees = invoice.fees + invoice.update(payment_status: 'succeeded') + + # estimate and create credit notes for first three items - full refund; the taxes are rounded to higher number + 3.times do |i| + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[i].id, + amount_cents: 68_33 + } + ] + ) + + # Estimate the credit notes amount on one fee rounds the taxes to higher number + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: "1366.6", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8200, + max_refundable_amount_cents: 8200, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 82_00, + items: [ + { + fee_id: fees[i].id, + amount_cents: 68_33 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 82_00, + total_amount_cents: 82_00, + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: 1366.6, + ) + expect(credit_note.precise_total).to eq(8199.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + end + # this value is wrong because of all rounding because if we subtract issued credit notes from the invoice, it + # will result in 327_98 - 82_00 * 3 = 81_98 + expect(invoice.creditable_amount_cents).to eq(8200) + + # split last refundable item into three chunks, first's taxes are rounded to lower number + # next two are rounded to higher number + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN1 + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[3].id, + amount_cents: 13_67 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 273, + precise_taxes_amount_cents: "273.4", + sub_total_excluding_taxes_amount_cents: 1367, + max_creditable_amount_cents: 1640, + max_refundable_amount_cents: 1640, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 1640, + items: [ + { + fee_id: fees[3].id, + amount_cents: 1367 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 1640, + total_amount_cents: 1640, + taxes_amount_cents: 273, + precise_taxes_amount_cents: 273.4, + ) + expect(credit_note.precise_total).to eq(1640.4) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) + # real remaining: 81_98 - 16_40 = 65_58 + expect(invoice.creditable_amount_cents).to eq(6559) + + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN2 + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[3].id, + amount_cents: 22_33 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 447, + precise_taxes_amount_cents: "446.6", + sub_total_excluding_taxes_amount_cents: 2233, + max_creditable_amount_cents: 2680, + max_refundable_amount_cents: 2680, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 2680, + items: [ + { + fee_id: fees[3].id, + amount_cents: 2233 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 2680, + total_amount_cents: 2680, + taxes_amount_cents: 447, + precise_taxes_amount_cents: 446.6, + ) + expect(credit_note.precise_total).to eq(2679.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + # real remaining: 65_58 - 26_80 = 38_78 + expect(invoice.creditable_amount_cents).to eq(3880) + + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN3 + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[3].id, + amount_cents: 32_33 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 645, + precise_taxes_amount_cents: "645.4", + sub_total_excluding_taxes_amount_cents: 3233, + max_creditable_amount_cents: 3878, + max_refundable_amount_cents: 3878, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 3878, + items: [ + { + fee_id: fees[3].id, + amount_cents: 3233 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 3878, + total_amount_cents: 3878, + taxes_amount_cents: 645, + precise_taxes_amount_cents: 645.4, + ) + expect(credit_note.precise_total).to eq(3878.4) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) + expect(invoice.creditable_amount_cents).to eq(0) + end + end + end end + diff --git a/spec/support/scenarios_helper.rb b/spec/support/scenarios_helper.rb index 6e304980957..72111fda2a0 100644 --- a/spec/support/scenarios_helper.rb +++ b/spec/support/scenarios_helper.rb @@ -69,6 +69,28 @@ def update_invoice(invoice, params) put_with_token(organization, "/api/v1/invoices/#{invoice.id}", {invoice: params}) end + def create_one_off_invoice(customer,addons) + create_invoice_params = { + customer: customer, + currency: "EUR", + fees: [], + timestamp: Time.zone.now.to_i + } + addons.each do |fee| + fee_addon_params = { + "add_on_id": fee.id, + "name": fee.name, + "units": 1, + "unit_amount_cents": fee.amount_cents, + "tax_codes": [ + tax.code + ] + } + create_invoice_params[:fees].push(fee_addon_params) + end + Invoices::CreateOneOffService.call(**create_invoice_params) + end + ### Coupons def create_coupon(params) From dd49ae5ebf1161ed862eb132333152a3a9795499 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 4 Oct 2024 13:28:52 +0200 Subject: [PATCH 6/9] add one more scenario --- app/models/credit_note.rb | 2 +- spec/models/credit_note_spec.rb | 5 +- spec/scenarios/credit_note_spec.rb | 444 +++++++++++++++++++++-------- spec/support/scenarios_helper.rb | 13 +- 4 files changed, 337 insertions(+), 127 deletions(-) diff --git a/app/models/credit_note.rb b/app/models/credit_note.rb index 07bc84b790c..61fc9e3f950 100644 --- a/app/models/credit_note.rb +++ b/app/models/credit_note.rb @@ -130,7 +130,7 @@ def sub_total_excluding_taxes_amount_cents alias_method :sub_total_excluding_taxes_amount_currency, :currency def precise_total - items.sum(&:precise_amount_cents) + precise_taxes_amount_cents + items.sum(&:precise_amount_cents) - precise_coupons_adjustment_amount_cents + precise_taxes_amount_cents end def taxes_rounding_adjustment diff --git a/spec/models/credit_note_spec.rb b/spec/models/credit_note_spec.rb index 4b7abdfe618..8a9861ce4e6 100644 --- a/spec/models/credit_note_spec.rb +++ b/spec/models/credit_note_spec.rb @@ -5,7 +5,7 @@ RSpec.describe CreditNote, type: :model do subject(:credit_note) do create :credit_note, credit_amount_cents: 11000, total_amount_cents: 11000, taxes_amount_cents: 1000, - taxes_rate: 10.0, precise_taxes_amount_cents: 1000 + taxes_rate: 10.0, precise_taxes_amount_cents: 1000 end let(:item) { create(:credit_note_item, credit_note:, precise_amount_cents: 10000, amount_cents: 1000) } @@ -357,7 +357,6 @@ end end - context 'when taxes are not precise' do subject(:credit_note) do create :credit_note, credit_amount_cents: 8200, total_amount_cents: 8200, taxes_amount_cents: 1367, @@ -389,5 +388,3 @@ end end end - - diff --git a/spec/scenarios/credit_note_spec.rb b/spec/scenarios/credit_note_spec.rb index 529e88241f7..d86c07f9df7 100644 --- a/spec/scenarios/credit_note_spec.rb +++ b/spec/scenarios/credit_note_spec.rb @@ -337,127 +337,119 @@ end end - context 'when creating credit notes for small items with taxes, so sum of items with their taxes is bigger than invoice total amount' do - let(:tax) { create(:tax, organization:, rate: 20) } - - context 'two similar items are refunded separately' do - let(:add_ons) do - [ create(:add_on, organization:, amount_cents: 68_33), - create(:add_on, organization:, amount_cents: 68_33)] - end - - it 'solves the rounding issue' do - # create a one off invoice with two addons and small amounts as feed - create_one_off_invoice(customer, add_ons) - # invoice amount should be with taxes calculated on items sum: - invoice = customer.invoices.order(:created_at).last - expect(invoice.total_amount_cents).to eq(163_99) - expect(invoice.taxes_amount_cents).to eq(27_33) - fees = invoice.fees - invoice.update(payment_status: 'succeeded') - - # estimate and create credit notes for first item - full refund; the taxes are rounded to higher number - estimate_credit_note( - invoice_id: invoice.id, - items: [ - { - fee_id: fees[0].id, - amount_cents: 68_33 - } - ] - ) - - # Estimate the credit notes amount on one fee rounds the taxes to higher number - estimate = json[:estimated_credit_note] - expect(estimate).to include( - taxes_amount_cents: 13_67, - precise_taxes_amount_cents: "1366.6", - sub_total_excluding_taxes_amount_cents: 6833, - max_creditable_amount_cents: 8200, - max_refundable_amount_cents: 8200, - taxes_rate: 20.0 - ) + context 'when creating credit note with possible rounding issues' do + context 'when creating credit notes for small items with taxes, so sum of items with their taxes is bigger than invoice total amount' do + let(:tax) { create(:tax, organization:, rate: 20) } + + context 'when two similar items are refunded separately' do + let(:add_ons) { create_list(:add_on, 2, organization:, amount_cents: 68_33) } + + it 'solves the rounding issue' do + # create a one off invoice with two addons and small amounts as feed + create_one_off_invoice(customer, add_ons) + # invoice amount should be with taxes calculated on items sum: + invoice = customer.invoices.order(:created_at).last + expect(invoice.total_amount_cents).to eq(163_99) + expect(invoice.taxes_amount_cents).to eq(27_33) + fees = invoice.fees + invoice.update(payment_status: 'succeeded') + + # estimate and create credit notes for first item - full refund; the taxes are rounded to higher number + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[0].id, + amount_cents: 68_33 + } + ] + ) - # Emit a credit note on only one fee - create_credit_note( - invoice_id: invoice.id, - reason: :other, - credit_amount_cents: 0, - refund_amount_cents: 82_00, - items: [ - { - fee_id: fees[0].id, - amount_cents: 68_33 - } - ] - ) + # Estimate the credit notes amount on one fee rounds the taxes to higher number + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: "1366.6", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8200, + max_refundable_amount_cents: 8200, + taxes_rate: 20.0 + ) - credit_note = invoice.credit_notes.order(:created_at).last - expect(credit_note).to have_attributes( - refund_amount_cents: 82_00, - total_amount_cents: 82_00, - taxes_amount_cents: 13_67, - precise_taxes_amount_cents: 1366.6, - ) - expect(credit_note.precise_total).to eq(8199.6) - expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 82_00, + items: [ + { + fee_id: fees[0].id, + amount_cents: 68_33 + } + ] + ) - #when issuing second credit note, it should be rounded to lower number - estimate_credit_note( - invoice_id: invoice.id, - items: [ - { - fee_id: fees[1].id, - amount_cents: 68_33 - } - ] - ) + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 82_00, + total_amount_cents: 82_00, + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: 1366.6 + ) + expect(credit_note.precise_total).to eq(8199.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) - estimate = json[:estimated_credit_note] - expect(estimate).to include( - taxes_amount_cents: 13_66, - precise_taxes_amount_cents: "1366.2", - sub_total_excluding_taxes_amount_cents: 6833, - max_creditable_amount_cents: 8199, - max_refundable_amount_cents: 8199, - taxes_rate: 20.0 - ) + # when issuing second credit note, it should be rounded to lower number + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[1].id, + amount_cents: 68_33 + } + ] + ) - # Emit a credit note on only one fee - create_credit_note( - invoice_id: invoice.id, - reason: :other, - credit_amount_cents: 0, - refund_amount_cents: 81_99, - items: [ - { - fee_id: fees[1].id, - amount_cents: 68_33 - } - ] - ) + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_66, + precise_taxes_amount_cents: "1366.2", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8199, + max_refundable_amount_cents: 8199, + taxes_rate: 20.0 + ) - credit_note = invoice.credit_notes.order(:created_at).last - expect(credit_note).to have_attributes( - refund_amount_cents: 8199, - total_amount_cents: 8199, - taxes_amount_cents: 13_66, - precise_taxes_amount_cents: 1366.2, - ) - expect(credit_note.precise_total).to eq(8199.2) - expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) - end + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 81_99, + items: [ + { + fee_id: fees[1].id, + amount_cents: 68_33 + } + ] + ) - end + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 8199, + total_amount_cents: 8199, + taxes_amount_cents: 13_66, + precise_taxes_amount_cents: 1366.2 + ) + expect(credit_note.precise_total).to eq(8199.2) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) + end - context 'four items are refunded separately, some whole, some in parts' do - let(:add_ons) do - [ create(:add_on, organization:, amount_cents: 68_33), - create(:add_on, organization:, amount_cents: 68_33), - create(:add_on, organization:, amount_cents: 68_33), - create(:add_on, organization:, amount_cents: 68_33)] end + context 'when four items are refunded separately, some whole, some in parts' do + let(:add_ons) { create_list(:add_on, 4, organization:, amount_cents: 68_33) } it 'solves the rounding issue' do # create a one off invoice with two addons and small amounts as feed @@ -511,7 +503,7 @@ refund_amount_cents: 82_00, total_amount_cents: 82_00, taxes_amount_cents: 13_67, - precise_taxes_amount_cents: 1366.6, + precise_taxes_amount_cents: 1366.6 ) expect(credit_note.precise_total).to eq(8199.6) expect(credit_note.taxes_rounding_adjustment).to eq(0.4) @@ -563,7 +555,7 @@ refund_amount_cents: 1640, total_amount_cents: 1640, taxes_amount_cents: 273, - precise_taxes_amount_cents: 273.4, + precise_taxes_amount_cents: 273.4 ) expect(credit_note.precise_total).to eq(1640.4) expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) @@ -611,7 +603,7 @@ refund_amount_cents: 2680, total_amount_cents: 2680, taxes_amount_cents: 447, - precise_taxes_amount_cents: 446.6, + precise_taxes_amount_cents: 446.6 ) expect(credit_note.precise_total).to eq(2679.6) expect(credit_note.taxes_rounding_adjustment).to eq(0.4) @@ -659,13 +651,233 @@ refund_amount_cents: 3878, total_amount_cents: 3878, taxes_amount_cents: 645, - precise_taxes_amount_cents: 645.4, + precise_taxes_amount_cents: 645.4 ) expect(credit_note.precise_total).to eq(3878.4) expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) expect(invoice.creditable_amount_cents).to eq(0) end end + end + + context 'when creating credit note with small items and applied coupons' do + let(:tax) { create(:tax, organization:, rate: 20) } + let(:plan_tax) { create(:tax, organization:, name: 'Plan Tax', rate: 20, applied_to_organization: false) } + let(:plan) do + create( + :plan, + organization:, + interval: :monthly, + amount_cents: 1_999, + pay_in_advance: false + ) + end + + let(:charge1) do + create( + :standard_charge, + plan:, + min_amount_cents: 6833 + ) + end + + let(:charge2) do + create( + :standard_charge, + plan:, + min_amount_cents: 200_33 + ) + end + + let(:coupon) do + create( + :coupon, + organization:, + amount_cents: 10_00, + expiration: :no_expiration, + coupon_type: :fixed_amount, + frequency: :forever, + limited_plans: false, + reusable: true + ) + end + + before do + charge1 + charge2 + end + + it 'calculates all roundings' do + # Creates two subscriptions + travel_to(DateTime.new(2022, 12, 19, 12)) do + create_subscription( + external_customer_id: customer.external_id, + external_id: "#{customer.external_id}_1", + plan_code: plan.code, + billing_time: :anniversary + ) + end + + # Apply a coupon twice to the customer + travel_to(DateTime.new(2023, 8, 29)) do + apply_coupon( + external_customer_id: customer.external_id, + coupon_code: coupon.code, + amount_cents: 10_00 + ) + end + + # Bill subscription on an anniversary date + travel_to(DateTime.new(2023, 10, 19)) do + Subscriptions::BillingService.call + perform_all_enqueued_jobs + end + + invoice = customer.invoices.order(created_at: :desc).first + # fees sum = 19_99 + 68_33 + 200_33 = 288_65 + # applied coupon - 10_00 + # subtotal before taxes - 278_65 + # taxes = 5573 + expect(invoice.total_amount_cents).to eq(334_38) + + # issue a CN for the full subscription fee - 19_99 before taxes and coupons + subscription_fee = invoice.fees.find(&:subscription?) + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: subscription_fee.id, + amount_cents: 19_99 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 386, + precise_taxes_amount_cents: "386.0", + sub_total_excluding_taxes_amount_cents: 1930, + max_creditable_amount_cents: 2316, + coupons_adjustment_amount_cents: 69, + taxes_rate: 20.0 + ) + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 23_16, + items: [ + { + fee_id: subscription_fee.id, + amount_cents: 19_99 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + credit_amount_cents: 2316, + total_amount_cents: 2316, + taxes_amount_cents: 386, + precise_taxes_amount_cents: 386.0, + precise_coupons_adjustment_amount_cents: 69.25342 + ) + expect(credit_note.precise_total).to eq(2315.74658) + expect(credit_note.taxes_rounding_adjustment).to eq(0) + # real remaining: 334_38 - 23_16 = 311_22 + expect(invoice.creditable_amount_cents).to eq(31122.253421098216) + + # issue a CN for the full first charge - 68_33 before taxes and coupons + first_charge = invoice.fees.find{|fee| fee.amount_cents == 68_33} + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: first_charge.id, + amount_cents: 68_33 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 1319, + precise_taxes_amount_cents: "1319.2", + sub_total_excluding_taxes_amount_cents: 6596, + max_creditable_amount_cents: 7915, + coupons_adjustment_amount_cents: 237, + taxes_rate: 20.0 + ) + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 7915, + items: [ + { + fee_id: first_charge.id, + amount_cents: 6833 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + credit_amount_cents: 7915, + total_amount_cents: 7915, + taxes_amount_cents: 1319, + precise_taxes_amount_cents: 1319.2, + precise_coupons_adjustment_amount_cents: 236.72267 + ) + expect(credit_note.precise_total).to eq(7915.47733) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) + # real remaining: 311_22 - 79_15 = 232_07 + expect(invoice.creditable_amount_cents).to eq(23206.97609561753) + + # issue a CN for the full last charge - 200_33 before taxes and coupons + last_charge = invoice.fees.find{|fee| fee.amount_cents == 200_33} + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: last_charge.id, + amount_cents: 200_33 + } + ] + ) + + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 3868, + precise_taxes_amount_cents: "3868.0", + sub_total_excluding_taxes_amount_cents: 19339, + max_creditable_amount_cents: 23207, + coupons_adjustment_amount_cents: 694, + taxes_rate: 20.0 + ) + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 23207, + items: [ + { + fee_id: last_charge.id, + amount_cents: 200_33 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + credit_amount_cents: 23207, + total_amount_cents: 23207, + taxes_amount_cents: 3868, + precise_taxes_amount_cents: 3868.0, + precise_coupons_adjustment_amount_cents: 694.0239 + ) + expect(credit_note.precise_total).to eq(23206.9761) + expect(credit_note.taxes_rounding_adjustment).to eq(0) + # real remaining: 232_07 - 23_207 = 0 + expect(invoice.creditable_amount_cents).to eq(0) + end + end end end - diff --git a/spec/support/scenarios_helper.rb b/spec/support/scenarios_helper.rb index 72111fda2a0..75d5e1da4a4 100644 --- a/spec/support/scenarios_helper.rb +++ b/spec/support/scenarios_helper.rb @@ -69,7 +69,7 @@ def update_invoice(invoice, params) put_with_token(organization, "/api/v1/invoices/#{invoice.id}", {invoice: params}) end - def create_one_off_invoice(customer,addons) + def create_one_off_invoice(customer, addons) create_invoice_params = { customer: customer, currency: "EUR", @@ -78,11 +78,12 @@ def create_one_off_invoice(customer,addons) } addons.each do |fee| fee_addon_params = { - "add_on_id": fee.id, - "name": fee.name, - "units": 1, - "unit_amount_cents": fee.amount_cents, - "tax_codes": [ + add_on_id: fee.id, + add_on_code: fee.code, + name: fee.name, + units: 1, + unit_amount_cents: fee.amount_cents, + tax_codes: [ tax.code ] } From 2507dad4c9b8877a243ffe1a5a3587e9b5682ca4 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 4 Oct 2024 14:26:40 +0200 Subject: [PATCH 7/9] linter fixes --- spec/scenarios/credit_note_spec.rb | 377 ++++++++++++++--------------- 1 file changed, 188 insertions(+), 189 deletions(-) diff --git a/spec/scenarios/credit_note_spec.rb b/spec/scenarios/credit_note_spec.rb index d86c07f9df7..b4fda2cc26b 100644 --- a/spec/scenarios/credit_note_spec.rb +++ b/spec/scenarios/credit_note_spec.rb @@ -445,42 +445,93 @@ expect(credit_note.precise_total).to eq(8199.2) expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) end - end context 'when four items are refunded separately, some whole, some in parts' do - let(:add_ons) { create_list(:add_on, 4, organization:, amount_cents: 68_33) } - - it 'solves the rounding issue' do - # create a one off invoice with two addons and small amounts as feed - create_one_off_invoice(customer, add_ons) - # invoice amount should be with taxes calculated on items sum: - invoice = customer.invoices.order(:created_at).last - expect(invoice.total_amount_cents).to eq(327_98) - expect(invoice.taxes_amount_cents).to eq(54_66) - fees = invoice.fees - invoice.update(payment_status: 'succeeded') - - # estimate and create credit notes for first three items - full refund; the taxes are rounded to higher number - 3.times do |i| + let(:add_ons) { create_list(:add_on, 4, organization:, amount_cents: 68_33) } + + it 'solves the rounding issue' do + # create a one off invoice with two addons and small amounts as feed + create_one_off_invoice(customer, add_ons) + # invoice amount should be with taxes calculated on items sum: + invoice = customer.invoices.order(:created_at).last + expect(invoice.total_amount_cents).to eq(327_98) + expect(invoice.taxes_amount_cents).to eq(54_66) + fees = invoice.fees + invoice.update(payment_status: 'succeeded') + + # estimate and create credit notes for first three items - full refund; the taxes are rounded to higher number + 3.times do |i| + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[i].id, + amount_cents: 68_33 + } + ] + ) + + # Estimate the credit notes amount on one fee rounds the taxes to higher number + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: "1366.6", + sub_total_excluding_taxes_amount_cents: 6833, + max_creditable_amount_cents: 8200, + max_refundable_amount_cents: 8200, + taxes_rate: 20.0 + ) + + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 82_00, + items: [ + { + fee_id: fees[i].id, + amount_cents: 68_33 + } + ] + ) + + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 82_00, + total_amount_cents: 82_00, + taxes_amount_cents: 13_67, + precise_taxes_amount_cents: 1366.6 + ) + expect(credit_note.precise_total).to eq(8199.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + end + # this value is wrong because of all rounding because if we subtract issued credit notes from the invoice, it + # will result in 327_98 - 82_00 * 3 = 81_98 + expect(invoice.creditable_amount_cents).to eq(8200) + + # split last refundable item into three chunks, first's taxes are rounded to lower number + # next two are rounded to higher number + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN1 estimate_credit_note( invoice_id: invoice.id, items: [ { - fee_id: fees[i].id, - amount_cents: 68_33 + fee_id: fees[3].id, + amount_cents: 13_67 } ] ) - # Estimate the credit notes amount on one fee rounds the taxes to higher number estimate = json[:estimated_credit_note] expect(estimate).to include( - taxes_amount_cents: 13_67, - precise_taxes_amount_cents: "1366.6", - sub_total_excluding_taxes_amount_cents: 6833, - max_creditable_amount_cents: 8200, - max_refundable_amount_cents: 8200, + taxes_amount_cents: 273, + precise_taxes_amount_cents: "273.4", + sub_total_excluding_taxes_amount_cents: 1367, + max_creditable_amount_cents: 1640, + max_refundable_amount_cents: 1640, taxes_rate: 20.0 ) @@ -489,176 +540,124 @@ invoice_id: invoice.id, reason: :other, credit_amount_cents: 0, - refund_amount_cents: 82_00, + refund_amount_cents: 1640, items: [ { - fee_id: fees[i].id, - amount_cents: 68_33 + fee_id: fees[3].id, + amount_cents: 1367 } ] ) credit_note = invoice.credit_notes.order(:created_at).last expect(credit_note).to have_attributes( - refund_amount_cents: 82_00, - total_amount_cents: 82_00, - taxes_amount_cents: 13_67, - precise_taxes_amount_cents: 1366.6 + refund_amount_cents: 1640, + total_amount_cents: 1640, + taxes_amount_cents: 273, + precise_taxes_amount_cents: 273.4 ) - expect(credit_note.precise_total).to eq(8199.6) - expect(credit_note.taxes_rounding_adjustment).to eq(0.4) - end - # this value is wrong because of all rounding because if we subtract issued credit notes from the invoice, it - # will result in 327_98 - 82_00 * 3 = 81_98 - expect(invoice.creditable_amount_cents).to eq(8200) - - # split last refundable item into three chunks, first's taxes are rounded to lower number - # next two are rounded to higher number - # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 - # CN1 - estimate_credit_note( - invoice_id: invoice.id, - items: [ - { - fee_id: fees[3].id, - amount_cents: 13_67 - } - ] - ) - - estimate = json[:estimated_credit_note] - expect(estimate).to include( - taxes_amount_cents: 273, - precise_taxes_amount_cents: "273.4", - sub_total_excluding_taxes_amount_cents: 1367, - max_creditable_amount_cents: 1640, - max_refundable_amount_cents: 1640, - taxes_rate: 20.0 - ) + expect(credit_note.precise_total).to eq(1640.4) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) + # real remaining: 81_98 - 16_40 = 65_58 + expect(invoice.creditable_amount_cents).to eq(6559) - # Emit a credit note on only one fee - create_credit_note( - invoice_id: invoice.id, - reason: :other, - credit_amount_cents: 0, - refund_amount_cents: 1640, - items: [ - { - fee_id: fees[3].id, - amount_cents: 1367 - } - ] - ) - - credit_note = invoice.credit_notes.order(:created_at).last - expect(credit_note).to have_attributes( - refund_amount_cents: 1640, - total_amount_cents: 1640, - taxes_amount_cents: 273, - precise_taxes_amount_cents: 273.4 - ) - expect(credit_note.precise_total).to eq(1640.4) - expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) - # real remaining: 81_98 - 16_40 = 65_58 - expect(invoice.creditable_amount_cents).to eq(6559) - - # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 - # CN2 - estimate_credit_note( - invoice_id: invoice.id, - items: [ - { - fee_id: fees[3].id, - amount_cents: 22_33 - } - ] - ) + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN2 + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[3].id, + amount_cents: 22_33 + } + ] + ) - estimate = json[:estimated_credit_note] - expect(estimate).to include( - taxes_amount_cents: 447, - precise_taxes_amount_cents: "446.6", - sub_total_excluding_taxes_amount_cents: 2233, - max_creditable_amount_cents: 2680, - max_refundable_amount_cents: 2680, - taxes_rate: 20.0 - ) + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 447, + precise_taxes_amount_cents: "446.6", + sub_total_excluding_taxes_amount_cents: 2233, + max_creditable_amount_cents: 2680, + max_refundable_amount_cents: 2680, + taxes_rate: 20.0 + ) - # Emit a credit note on only one fee - create_credit_note( - invoice_id: invoice.id, - reason: :other, - credit_amount_cents: 0, - refund_amount_cents: 2680, - items: [ - { - fee_id: fees[3].id, - amount_cents: 2233 - } - ] - ) + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 2680, + items: [ + { + fee_id: fees[3].id, + amount_cents: 2233 + } + ] + ) - credit_note = invoice.credit_notes.order(:created_at).last - expect(credit_note).to have_attributes( - refund_amount_cents: 2680, - total_amount_cents: 2680, - taxes_amount_cents: 447, - precise_taxes_amount_cents: 446.6 - ) - expect(credit_note.precise_total).to eq(2679.6) - expect(credit_note.taxes_rounding_adjustment).to eq(0.4) - # real remaining: 65_58 - 26_80 = 38_78 - expect(invoice.creditable_amount_cents).to eq(3880) + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 2680, + total_amount_cents: 2680, + taxes_amount_cents: 447, + precise_taxes_amount_cents: 446.6 + ) + expect(credit_note.precise_total).to eq(2679.6) + expect(credit_note.taxes_rounding_adjustment).to eq(0.4) + # real remaining: 65_58 - 26_80 = 38_78 + expect(invoice.creditable_amount_cents).to eq(3880) - # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 - # CN3 - estimate_credit_note( - invoice_id: invoice.id, - items: [ - { - fee_id: fees[3].id, - amount_cents: 32_33 - } - ] - ) + # cn_1 => 13.67, cn2 => 22.33, cn3 => 32.33 + # CN3 + estimate_credit_note( + invoice_id: invoice.id, + items: [ + { + fee_id: fees[3].id, + amount_cents: 32_33 + } + ] + ) - estimate = json[:estimated_credit_note] - expect(estimate).to include( - taxes_amount_cents: 645, - precise_taxes_amount_cents: "645.4", - sub_total_excluding_taxes_amount_cents: 3233, - max_creditable_amount_cents: 3878, - max_refundable_amount_cents: 3878, - taxes_rate: 20.0 - ) + estimate = json[:estimated_credit_note] + expect(estimate).to include( + taxes_amount_cents: 645, + precise_taxes_amount_cents: "645.4", + sub_total_excluding_taxes_amount_cents: 3233, + max_creditable_amount_cents: 3878, + max_refundable_amount_cents: 3878, + taxes_rate: 20.0 + ) - # Emit a credit note on only one fee - create_credit_note( - invoice_id: invoice.id, - reason: :other, - credit_amount_cents: 0, - refund_amount_cents: 3878, - items: [ - { - fee_id: fees[3].id, - amount_cents: 3233 - } - ] - ) + # Emit a credit note on only one fee + create_credit_note( + invoice_id: invoice.id, + reason: :other, + credit_amount_cents: 0, + refund_amount_cents: 3878, + items: [ + { + fee_id: fees[3].id, + amount_cents: 3233 + } + ] + ) - credit_note = invoice.credit_notes.order(:created_at).last - expect(credit_note).to have_attributes( - refund_amount_cents: 3878, - total_amount_cents: 3878, - taxes_amount_cents: 645, - precise_taxes_amount_cents: 645.4 - ) - expect(credit_note.precise_total).to eq(3878.4) - expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) - expect(invoice.creditable_amount_cents).to eq(0) + credit_note = invoice.credit_notes.order(:created_at).last + expect(credit_note).to have_attributes( + refund_amount_cents: 3878, + total_amount_cents: 3878, + taxes_amount_cents: 645, + precise_taxes_amount_cents: 645.4 + ) + expect(credit_note.precise_total).to eq(3878.4) + expect(credit_note.taxes_rounding_adjustment).to eq(-0.4) + expect(invoice.creditable_amount_cents).to eq(0) + end end end - end context 'when creating credit note with small items and applied coupons' do let(:tax) { create(:tax, organization:, rate: 20) } @@ -787,7 +786,7 @@ expect(invoice.creditable_amount_cents).to eq(31122.253421098216) # issue a CN for the full first charge - 68_33 before taxes and coupons - first_charge = invoice.fees.find{|fee| fee.amount_cents == 68_33} + first_charge = invoice.fees.find { |fee| fee.amount_cents == 68_33 } estimate_credit_note( invoice_id: invoice.id, items: [ @@ -800,13 +799,13 @@ estimate = json[:estimated_credit_note] expect(estimate).to include( - taxes_amount_cents: 1319, - precise_taxes_amount_cents: "1319.2", - sub_total_excluding_taxes_amount_cents: 6596, - max_creditable_amount_cents: 7915, - coupons_adjustment_amount_cents: 237, - taxes_rate: 20.0 - ) + taxes_amount_cents: 1319, + precise_taxes_amount_cents: "1319.2", + sub_total_excluding_taxes_amount_cents: 6596, + max_creditable_amount_cents: 7915, + coupons_adjustment_amount_cents: 237, + taxes_rate: 20.0 + ) create_credit_note( invoice_id: invoice.id, reason: :other, @@ -821,19 +820,19 @@ credit_note = invoice.credit_notes.order(:created_at).last expect(credit_note).to have_attributes( - credit_amount_cents: 7915, - total_amount_cents: 7915, - taxes_amount_cents: 1319, - precise_taxes_amount_cents: 1319.2, - precise_coupons_adjustment_amount_cents: 236.72267 - ) + credit_amount_cents: 7915, + total_amount_cents: 7915, + taxes_amount_cents: 1319, + precise_taxes_amount_cents: 1319.2, + precise_coupons_adjustment_amount_cents: 236.72267 + ) expect(credit_note.precise_total).to eq(7915.47733) expect(credit_note.taxes_rounding_adjustment).to eq(-0.2) # real remaining: 311_22 - 79_15 = 232_07 expect(invoice.creditable_amount_cents).to eq(23206.97609561753) # issue a CN for the full last charge - 200_33 before taxes and coupons - last_charge = invoice.fees.find{|fee| fee.amount_cents == 200_33} + last_charge = invoice.fees.find { |fee| fee.amount_cents == 200_33 } estimate_credit_note( invoice_id: invoice.id, items: [ From c4e63fb1a1dff7a97ced3e38f2c558a980d40c30 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 4 Oct 2024 14:59:51 +0200 Subject: [PATCH 8/9] fix broken tests --- spec/models/credit_note_spec.rb | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/spec/models/credit_note_spec.rb b/spec/models/credit_note_spec.rb index 8a9861ce4e6..b1224154aaa 100644 --- a/spec/models/credit_note_spec.rb +++ b/spec/models/credit_note_spec.rb @@ -10,11 +10,6 @@ let(:item) { create(:credit_note_item, credit_note:, precise_amount_cents: 10000, amount_cents: 1000) } - before do - item - credit_note.reload - end - it_behaves_like 'paper_trail traceable' it { is_expected.to have_many(:integration_resources) } @@ -253,16 +248,23 @@ end end - describe '#sub_total_excluding_taxes_amount_cents' do - it 'returs the total amount without the taxes' do - expect(credit_note.sub_total_excluding_taxes_amount_cents) - .to eq(credit_note.items.sum(&:precise_amount_cents) - credit_note.precise_coupons_adjustment_amount_cents) + context 'when calculating depends on related items' do + before do + item + credit_note.reload end - end - describe '#precise_total' do - it 'returns the total precise amount including precise taxes' do - expect(credit_note.precise_total).to eq(11000) + describe '#sub_total_excluding_taxes_amount_cents' do + it 'returs the total amount without the taxes' do + expect(credit_note.sub_total_excluding_taxes_amount_cents) + .to eq(credit_note.items.sum(&:precise_amount_cents) - credit_note.precise_coupons_adjustment_amount_cents) + end + end + + describe '#precise_total' do + it 'returns the total precise amount including precise taxes' do + expect(credit_note.precise_total).to eq(11000) + end end end @@ -367,6 +369,7 @@ before do item + credit_note.reload end describe '#precise_total' do From ea9bf92945fa496a7abd6923aa234e8149a01c51 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 8 Oct 2024 16:06:29 +0200 Subject: [PATCH 9/9] clean the code --- app/models/invoice.rb | 4 +--- app/services/credit_notes/estimate_service.rb | 1 - .../credit_notes/validate_item_service.rb | 3 --- .../credit_notes/validate_item_service_spec.rb | 16 ---------------- spec/support/scenarios_helper.rb | 4 ++-- 5 files changed, 3 insertions(+), 25 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 15df51514c2..44c58312fd4 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -272,9 +272,7 @@ def creditable_amount_cents fee_rate = fee.creditable_amount_cents.fdiv(fees_total_creditable) prorated_credit_amount = credit_adjustement * fee_rate (fee.creditable_amount_cents - prorated_credit_amount) * (fee.taxes_rate || 0) - end.fdiv(100).round - # end.fdiv(100) - # BECAUSE OF THIS ROUND the returned value is not precise + end.fdiv(100).round # BECAUSE OF THIS ROUND the returned value is not precise fees_total_creditable - credit_adjustement + vat end diff --git a/app/services/credit_notes/estimate_service.rb b/app/services/credit_notes/estimate_service.rb index e655ec381c3..cd37812b9ad 100644 --- a/app/services/credit_notes/estimate_service.rb +++ b/app/services/credit_notes/estimate_service.rb @@ -107,7 +107,6 @@ def all_rounding_tax_adjustments def compute_refundable_amount credit_note.refund_amount_cents = credit_note.credit_amount_cents - # invoice.refundable_amount_cents is incorrect - in our case it returns 8200, but it should be 8199... refundable_amount_cents = invoice.refundable_amount_cents return unless credit_note.credit_amount_cents > refundable_amount_cents diff --git a/app/services/credit_notes/validate_item_service.rb b/app/services/credit_notes/validate_item_service.rb index 3bf2af3f93e..cd0bb62f871 100644 --- a/app/services/credit_notes/validate_item_service.rb +++ b/app/services/credit_notes/validate_item_service.rb @@ -7,9 +7,6 @@ def valid? valid_item_amount? valid_individual_amount? - # we don't save taxes on the credit_note item and we'll adjust them when creating credit notes, so it makes sense - # to check taxes on credit note level, where taxes are calculated and stored. - # valid_global_amount? if errors? result.validation_failure!(errors:) diff --git a/spec/services/credit_notes/validate_item_service_spec.rb b/spec/services/credit_notes/validate_item_service_spec.rb index 3c2eed93a9a..1aeea95c14d 100644 --- a/spec/services/credit_notes/validate_item_service_spec.rb +++ b/spec/services/credit_notes/validate_item_service_spec.rb @@ -95,21 +95,5 @@ end end end - - # this check should not be on the item level, as taxes are applied and saved on the credit note level - # context 'when reaching invoice creditable amount' do - # before do - # create(:credit_note, invoice:, total_amount_cents: 99) - # end - # - # it 'fails the validation' do - # aggregate_failures do - # expect(validator).not_to be_valid - # - # expect(result.error).to be_a(BaseService::ValidationFailure) - # expect(result.error.messages[:amount_cents]).to eq(['higher_than_remaining_invoice_amount']) - # end - # end - # end end end diff --git a/spec/support/scenarios_helper.rb b/spec/support/scenarios_helper.rb index 75d5e1da4a4..29147e7a3cb 100644 --- a/spec/support/scenarios_helper.rb +++ b/spec/support/scenarios_helper.rb @@ -71,7 +71,7 @@ def update_invoice(invoice, params) def create_one_off_invoice(customer, addons) create_invoice_params = { - customer: customer, + external_customer_id: customer.external_id, currency: "EUR", fees: [], timestamp: Time.zone.now.to_i @@ -89,7 +89,7 @@ def create_one_off_invoice(customer, addons) } create_invoice_params[:fees].push(fee_addon_params) end - Invoices::CreateOneOffService.call(**create_invoice_params) + post_with_token(organization, "/api/v1/invoices", {invoice: create_invoice_params}) end ### Coupons