From e7f34a3a2e90b7641b8139ce23f2a76d16255fb4 Mon Sep 17 00:00:00 2001 From: Anton-Ivanov Date: Mon, 16 Sep 2024 07:29:58 +0300 Subject: [PATCH] 1554, introduce auto approve invoice && refactoring --- app/admin/billing/invoices.rb | 14 +++-- app/forms/manual_invoice_form.rb | 4 ++ app/models/billing/invoice.rb | 14 ----- app/services/billing_invoice/approve.rb | 37 +++++++++++++ app/services/billing_invoice/create.rb | 3 +- app/services/billing_invoice/fill.rb | 2 + app/services/billing_invoice/generate.rb | 17 ++++-- config/initializers/_config.rb | 4 ++ config/yeti_web.yml.ci | 2 + config/yeti_web.yml.development | 3 ++ config/yeti_web.yml.distr | 3 ++ spec/config/yeti_web_spec.rb | 5 +- .../invoices/approve_invoice_feature_spec.rb | 37 +++++++++++++ spec/services/billing_invoice/approve_spec.rb | 53 +++++++++++++++++++ spec/services/billing_invoice/create_spec.rb | 7 --- .../services/billing_invoice/generate_spec.rb | 41 ++++++++++++++ 16 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 app/services/billing_invoice/approve.rb create mode 100644 spec/features/billing/invoices/approve_invoice_feature_spec.rb create mode 100644 spec/services/billing_invoice/approve_spec.rb diff --git a/app/admin/billing/invoices.rb b/app/admin/billing/invoices.rb index b273a50e9..1cfb1c360 100644 --- a/app/admin/billing/invoices.rb +++ b/app/admin/billing/invoices.rb @@ -36,14 +36,12 @@ def scoped_collection end member_action :approve, method: :post do - if resource.approvable? - resource.approve - flash[:notice] = 'Invoice approved' - redirect_back fallback_location: root_path - else - flash[:notice] = 'Invoice can' 't be approved' - redirect_back fallback_location: root_path - end + BillingInvoice::Approve.call(invoice: resource) + flash[:notice] = 'Invoice was successful approved' + rescue BillingInvoice::Approve::Error => e + flash[:error] = e.message + ensure + redirect_back fallback_location: root_path end member_action :regenerate_document, method: :post do diff --git a/app/forms/manual_invoice_form.rb b/app/forms/manual_invoice_form.rb index 6c69dde3e..42f545d58 100644 --- a/app/forms/manual_invoice_form.rb +++ b/app/forms/manual_invoice_form.rb @@ -85,7 +85,11 @@ def _save end_time: end_time, type_id: Billing::InvoiceType::MANUAL ) + Worker::FillInvoiceJob.perform_later(@model.id) + @model rescue BillingInvoice::Create::Error => e errors.add(:base, e.message) + rescue Worker::FillInvoiceJob => e + errors.add(:base, e.message) end end diff --git a/app/models/billing/invoice.rb b/app/models/billing/invoice.rb index c5a400f27..36a7e781b 100644 --- a/app/models/billing/invoice.rb +++ b/app/models/billing/invoice.rb @@ -158,14 +158,6 @@ def display_name "Invoice #{id}" end - # todo service - def approve - transaction do - update!(state_id: Billing::InvoiceState::APPROVED) - send_email - end - end - def approvable? state.pending? end @@ -196,12 +188,6 @@ def subject display_name end - # FIX this copy paste - # todo service - def send_email - invoice_document&.send_invoice - end - private def validate_dates diff --git a/app/services/billing_invoice/approve.rb b/app/services/billing_invoice/approve.rb new file mode 100644 index 000000000..7d1c63aa8 --- /dev/null +++ b/app/services/billing_invoice/approve.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module BillingInvoice + class Approve < ApplicationService + parameter :invoice, required: true + + Error = Class.new(ApplicationService::Error) + + def call + Billing::Invoice.transaction do + raise_if_invalid! + + approve_invoice! + send_email + end + end + + private + + def approve_invoice! + invoice.update!(state_id: Billing::InvoiceState::APPROVED) + rescue ActiveRecord::RecordInvalid => e + raise Error, e.message + end + + def send_email + if invoice.invoice_document.present? + invoice.invoice_document.send_invoice + end + end + + def raise_if_invalid! + raise Error, 'Invoice already approved' if invoice.state_id == Billing::InvoiceState::APPROVED + raise Error, "Invoice can't be approved" unless invoice.approvable? + end + end +end diff --git a/app/services/billing_invoice/create.rb b/app/services/billing_invoice/create.rb index 37866b6d8..5588378a9 100644 --- a/app/services/billing_invoice/create.rb +++ b/app/services/billing_invoice/create.rb @@ -7,6 +7,8 @@ class Create < ApplicationService parameter :start_time, required: true parameter :end_time, required: true + Error = Class.new(ApplicationService::Error) + delegate :contractor, to: :account def call @@ -22,7 +24,6 @@ def call end_date: end_time ) invoice.update! reference: build_reference(invoice) - Worker::FillInvoiceJob.perform_later(invoice.id) invoice end rescue ActiveRecord::RecordInvalid => e diff --git a/app/services/billing_invoice/fill.rb b/app/services/billing_invoice/fill.rb index 173204f41..be8740792 100644 --- a/app/services/billing_invoice/fill.rb +++ b/app/services/billing_invoice/fill.rb @@ -4,6 +4,8 @@ module BillingInvoice class Fill < ApplicationService parameter :invoice, required: true + Error = Class.new(ApplicationService::Error) + def call AdvisoryLock::Cdr.with_lock(:invoice, id: invoice.account_id) do invoice.reload diff --git a/app/services/billing_invoice/generate.rb b/app/services/billing_invoice/generate.rb index 0331f5ced..bb7d784a1 100644 --- a/app/services/billing_invoice/generate.rb +++ b/app/services/billing_invoice/generate.rb @@ -4,6 +4,8 @@ module BillingInvoice class Generate < ApplicationService parameter :account, required: true + Error = Class.new(ApplicationService::Error) + # @return [Billing::Invoice] def call AdvisoryLock::Cdr.with_lock(:invoice, id: account.id) do @@ -19,15 +21,18 @@ def call end_time: account.next_invoice_at, type_id: account.next_invoice_type_id ) - + BillingInvoice::Fill.call(invoice: invoice) schedule_next_invoice!(invoice_params) + approve_invoice!(invoice) if YetiConfig.invoice&.auto_approve + invoice end rescue BillingInvoice::Create::Error => e raise Error, e.message + rescue BillingInvoice::Fill::Error => e + raise Error, e.message rescue ActiveRecord::RecordInvalid => e - message = e.record ? e.errors.full_messages.join(', ') : e.message - raise Error, message + raise Error, e.message end private @@ -55,5 +60,11 @@ def schedule_next_invoice!(invoice_params) next_invoice_type_id: invoice_params[:next_type_id] ) end + + def approve_invoice!(invoice) + BillingInvoice::Approve.call(invoice:) + rescue ActiveRecord::RecordInvalid => e + raise Error, e + end end end diff --git a/config/initializers/_config.rb b/config/initializers/_config.rb index b88d5ef99..63edaa825 100644 --- a/config/initializers/_config.rb +++ b/config/initializers/_config.rb @@ -90,6 +90,10 @@ def self.setting_files(config_root, _env) end optional(:customer_api_cdr_hide_fields).array(:string) + + optional(:invoice).schema do + optional(:auto_approve).value(:bool) + end end end diff --git a/config/yeti_web.yml.ci b/config/yeti_web.yml.ci index 9d6e252b7..c23f0a32e 100644 --- a/config/yeti_web.yml.ci +++ b/config/yeti_web.yml.ci @@ -53,3 +53,5 @@ cryptomus: routing_simulation_default_interface: internal +invoice: + auto_approve: false diff --git a/config/yeti_web.yml.development b/config/yeti_web.yml.development index bc7c3c3db..dbe23072c 100644 --- a/config/yeti_web.yml.development +++ b/config/yeti_web.yml.development @@ -48,3 +48,6 @@ api_log_tags: - SOME_TAG_FOR_API_LOG routing_simulation_default_interface: internal + +invoice: + auto_approve: false diff --git a/config/yeti_web.yml.distr b/config/yeti_web.yml.distr index 0a008190e..691941682 100644 --- a/config/yeti_web.yml.distr +++ b/config/yeti_web.yml.distr @@ -48,3 +48,6 @@ api_log_tags: - SOME_TAG_FOR_API_LOG routing_simulation_default_interface: internal + +invoice: + auto_approve: false diff --git a/spec/config/yeti_web_spec.rb b/spec/config/yeti_web_spec.rb index 2e24f7bf7..df4fbad51 100644 --- a/spec/config/yeti_web_spec.rb +++ b/spec/config/yeti_web_spec.rb @@ -56,7 +56,10 @@ url_callback: a_kind_of(String), url_return: a_kind_of(String) }, - routing_simulation_default_interface: a_kind_of(String) + routing_simulation_default_interface: a_kind_of(String), + invoice: { + auto_approve: boolean + } } end diff --git a/spec/features/billing/invoices/approve_invoice_feature_spec.rb b/spec/features/billing/invoices/approve_invoice_feature_spec.rb new file mode 100644 index 000000000..af5e6432a --- /dev/null +++ b/spec/features/billing/invoices/approve_invoice_feature_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe 'Approve invoide feature', type: :feature do + subject { click_action_item 'Approve' } + + include_context :login_as_admin + + context 'when valid data' do + let!(:invoice) { FactoryBot.create(:invoice, :manual, :pending, :with_vendor_account) } + let(:approved_state_id) { Billing::InvoiceState::APPROVED } + + before { visit invoice_path(invoice) } + + it 'should approve invoice', :js do + subject + + expect(page).to have_flash_message 'Invoice was successful approved', type: :notice + expect(invoice.reload).to have_attributes(state_id: approved_state_id) + end + end + + context 'when invalid data', :js do + let!(:invoice) { FactoryBot.create(:invoice, :pending, :with_vendor_account) } + + before do + allow(BillingInvoice::Approve).to receive(:call).and_raise(BillingInvoice::Approve::Error, 'error') + visit invoice_path(invoice) + end + + it 'should render error' do + subject + + expect(page).to have_flash_message 'error', type: :error + expect(BillingInvoice::Approve).to have_received(:call) + end + end +end diff --git a/spec/services/billing_invoice/approve_spec.rb b/spec/services/billing_invoice/approve_spec.rb new file mode 100644 index 000000000..ef5ce1261 --- /dev/null +++ b/spec/services/billing_invoice/approve_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe BillingInvoice::Approve, type: :service do + subject { described_class.call(service_params) } + + let(:service_params) { { invoice: } } + + context 'when valid data' do + let!(:contractor) { FactoryBot.create(:customer) } + let!(:contact) { FactoryBot.create(:contact, contractor:) } + let!(:account) { FactoryBot.create(:account, contractor:, send_invoices_to: [contact.id]) } + let(:invoice_attrs) { { account:, contractor: } } + let!(:invoice) { FactoryBot.create(:invoice, :pending, invoice_attrs) } + let(:invoice_document_attrs) { { invoice:, filename: "#{invoice.id}_#{invoice.start_date}_#{invoice.end_date}" } } + let!(:invoice_document) { FactoryBot.create(:invoice_document, :filled, invoice_document_attrs) } + + before { FactoryBot.create(:smtp_connection, global: true) } + + it 'approves invoice' do + expect { subject }.to change { invoice.state_id }.to(Billing::InvoiceState::APPROVED) + end + + it 'enqueues email worker' do + expect { subject }.to have_enqueued_job(Worker::SendEmailLogJob) + end + end + + context 'when invoice already approved' do + let!(:invoice) { FactoryBot.build_stubbed(:invoice, :approved) } + + it 'raises error' do + expect { subject }.to raise_error(BillingInvoice::Approve::Error, 'Invoice already approved') + end + end + + context 'when invoice is not approvable' do + let!(:invoice) { FactoryBot.build_stubbed(:invoice, :new) } + + it 'raises error' do + expect { subject }.to raise_error(BillingInvoice::Approve::Error, "Invoice can't be approved") + end + end + + context 'when validation error' do + let!(:invoice) { FactoryBot.create(:invoice, :pending, :with_vendor_account) } + + before { allow_any_instance_of(Billing::Invoice).to receive(:update!).and_raise(ActiveRecord::RecordInvalid) } + + it 'raises error' do + expect { subject }.to raise_error(BillingInvoice::Approve::Error, 'Record invalid') + end + end +end diff --git a/spec/services/billing_invoice/create_spec.rb b/spec/services/billing_invoice/create_spec.rb index 5117febc0..e900d32cd 100644 --- a/spec/services/billing_invoice/create_spec.rb +++ b/spec/services/billing_invoice/create_spec.rb @@ -47,13 +47,6 @@ uuid: be_present ) end - - it 'enqueues Worker::FillInvoiceJob with invoice.id' do - subject - invoice = Billing::Invoice.last! - - expect(Worker::FillInvoiceJob).to have_been_enqueued.with(invoice.id) - end end shared_examples :does_not_create_invoice do diff --git a/spec/services/billing_invoice/generate_spec.rb b/spec/services/billing_invoice/generate_spec.rb index df0c7d4cf..bd9fc773f 100644 --- a/spec/services/billing_invoice/generate_spec.rb +++ b/spec/services/billing_invoice/generate_spec.rb @@ -203,4 +203,45 @@ ) end end + + context 'when "invoice.auto_approve" setting has "true" value' do + before { allow(YetiConfig.invoice).to receive(:auto_approve).and_return(true) } + + it 'should create invoice with approved state' do + expect { subject }.to change(Billing::Invoice.approved, :count).by(1) + expect(YetiConfig.invoice).to have_received(:auto_approve).once + end + end + + context 'when the "invoice.auto_approve" setting has "false" value' do + before { allow(YetiConfig.invoice).to receive(:auto_approve).and_return(false) } + + it 'should create the "new" with approved state' do + expect { subject }.to change(Billing::Invoice.pending, :count).by(1) + end + end + + context 'when BillingInvoice::Create service return error' do + before { allow(BillingInvoice::Create).to receive(:call).and_raise(BillingInvoice::Create::Error, 'error') } + + it 'raises error' do + expect { subject }.to raise_error BillingInvoice::Generate::Error + end + end + + context 'when BillingInvoice::Fill service return error' do + before { allow(BillingInvoice::Fill).to receive(:call).and_raise(BillingInvoice::Fill::Error, 'error') } + + it 'raises error' do + expect { subject }.to raise_error BillingInvoice::Generate::Error + end + end + + context 'when validation error generated' do + before { allow(BillingInvoice::Create).to receive(:call).and_raise(ActiveRecord::RecordInvalid) } + + it 'raises error' do + expect { subject }.to raise_error BillingInvoice::Generate::Error + end + end end