From 7a46e51eee4a1c3d418651308405a00381bad1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Tue, 11 Jul 2023 17:30:25 +0200 Subject: [PATCH] feat(taxes): Assign tax to a charge --- app/controllers/api/v1/plans_controller.rb | 1 + app/services/charges/apply_taxes_service.rb | 35 +++++++++ app/services/plans/create_service.rb | 10 ++- spec/requests/api/v1/plans_spec.rb | 3 + .../charges/apply_taxes_service_spec.rb | 75 +++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 app/services/charges/apply_taxes_service.rb create mode 100644 spec/services/charges/apply_taxes_service_spec.rb diff --git a/app/controllers/api/v1/plans_controller.rb b/app/controllers/api/v1/plans_controller.rb index f3825bf6d42..72966485c91 100644 --- a/app/controllers/api/v1/plans_controller.rb +++ b/app/controllers/api/v1/plans_controller.rb @@ -97,6 +97,7 @@ def input_params { values: {} }, ], }, + { tax_codes: [] }, ], ) end diff --git a/app/services/charges/apply_taxes_service.rb b/app/services/charges/apply_taxes_service.rb new file mode 100644 index 00000000000..b1fe5170145 --- /dev/null +++ b/app/services/charges/apply_taxes_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Charges + class ApplyTaxesService < BaseService + def initialize(charge:, tax_codes:) + @charge = charge + @tax_codes = tax_codes + + super + end + + def call + return result.not_found_failure!(resource: 'charge') unless charge + return result.not_found_failure!(resource: 'tax') if (tax_codes - taxes.pluck(:code)).present? + + result.applied_taxes = tax_codes.map do |tax_code| + charge.applied_taxes.find_or_create_by!(tax: taxes.find_by(code: tax_code)) + end + + Invoices::RefreshBatchJob.perform_later(charge.plan.invoices.draft.pluck(:id)) + + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + private + + attr_reader :charge, :tax_codes + + def taxes + @taxes ||= charge.plan.organization.taxes.where(code: tax_codes) + end + end +end diff --git a/app/services/plans/create_service.rb b/app/services/plans/create_service.rb index 0d4f9aa311f..3b1d514df98 100644 --- a/app/services/plans/create_service.rb +++ b/app/services/plans/create_service.rb @@ -28,7 +28,15 @@ def create(**args) ActiveRecord::Base.transaction do plan.save! - args[:charges].each { |c| create_charge(plan, c) } if args[:charges].present? + if args[:charges].present? + args[:charges].each do |charge| + new_charge = create_charge(plan, charge) + + if charge[:tax_codes].present? + Charges::ApplyTaxesService.call(charge: new_charge, tax_codes: charge[:tax_codes]) + end + end + end end result.plan = plan diff --git a/spec/requests/api/v1/plans_spec.rb b/spec/requests/api/v1/plans_spec.rb index fdacfba6297..7b757b7eaca 100644 --- a/spec/requests/api/v1/plans_spec.rb +++ b/spec/requests/api/v1/plans_spec.rb @@ -3,8 +3,10 @@ require 'rails_helper' RSpec.describe Api::V1::PlansController, type: :request do + let(:tax) { create(:tax, organization:) } let(:organization) { create(:organization) } let(:billable_metric) { create(:billable_metric, organization:) } + let(:plan) { create(:plan, code: 'plan_code') } describe 'create' do let(:create_params) do @@ -24,6 +26,7 @@ properties: { amount: '0.22', }, + tax_codes: [tax.code], }, ], } diff --git a/spec/services/charges/apply_taxes_service_spec.rb b/spec/services/charges/apply_taxes_service_spec.rb new file mode 100644 index 00000000000..5cf57554e5b --- /dev/null +++ b/spec/services/charges/apply_taxes_service_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Charges::ApplyTaxesService, type: :service do + subject(:apply_service) { described_class.new(charge:, tax_codes:) } + + let(:plan) { create(:plan) } + let(:charge) { create(:standard_charge, plan:) } + let(:tax1) { create(:tax, organization: plan.organization, code: 'tax1') } + let(:tax2) { create(:tax, organization: plan.organization, code: 'tax2') } + let(:tax_codes) { [tax1.code, tax2.code] } + + describe 'call' do + it 'applies taxes to the charge' do + expect { apply_service.call }.to change { charge.applied_taxes.count }.from(0).to(2) + end + + it 'refreshes draft invoices' do + subscription = create(:subscription, plan:) + invoice = create(:invoice, :draft, organization: plan.organization) + create(:invoice_subscription, invoice:, subscription:) + + expect do + apply_service.call + end.to have_enqueued_job(Invoices::RefreshBatchJob).with([invoice.id]) + end + + it 'returns applied taxes' do + result = apply_service.call + expect(result.applied_taxes.count).to eq(2) + end + + context 'when charge is not found' do + let(:charge) { nil } + + it 'returns an error' do + result = apply_service.call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.error_code).to eq('charge_not_found') + end + end + end + + context 'when tax is not found' do + let(:tax_codes) { ['unknown'] } + + it 'returns an error' do + result = apply_service.call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.error_code).to eq('tax_not_found') + end + end + end + + context 'when applied tax is already present' do + it 'does not create a new applied tax' do + create(:charge_applied_tax, charge:, tax: tax1) + expect { apply_service.call }.to change { charge.applied_taxes.count }.from(1).to(2) + end + end + + context 'when trying to apply twice the same tax' do + let(:tax_codes) { [tax1.code, tax1.code] } + + it 'assigns it only once' do + expect { apply_service.call }.to change { charge.applied_taxes.count }.from(0).to(1) + end + end + end +end