From 91101b2e8b5d11b6152676948867b4dd93142a5d Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Mon, 22 Jul 2024 19:32:34 +0300 Subject: [PATCH] fix billing service renew --- app/jobs/jobs/service_renew.rb | 5 +- app/models/billing/service.rb | 2 +- app/models/billing/service/renew.rb | 44 ++++---- spec/jobs/jobs/service_renew_spec.rb | 49 +++++++++ spec/models/billing/service/renew_spec.rb | 116 ++++++++++++++++++++++ 5 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 spec/jobs/jobs/service_renew_spec.rb create mode 100644 spec/models/billing/service/renew_spec.rb diff --git a/app/jobs/jobs/service_renew.rb b/app/jobs/jobs/service_renew.rb index 002611c5e..29112c27f 100644 --- a/app/jobs/jobs/service_renew.rb +++ b/app/jobs/jobs/service_renew.rb @@ -6,13 +6,14 @@ class ServiceRenew < ::BaseJob def execute Billing::Service.ready_for_renew.find_each do |service| - Billing::Service::Renew.new(service).perform + renew(service) end end def renew(service) - Billing::Service::Renew.new(service).perform + Billing::Service::Renew.perform(service) rescue StandardError => e + log_error(e) capture_error(e, extra: { service_id: service.id }) end end diff --git a/app/models/billing/service.rb b/app/models/billing/service.rb index 51075ad04..10859f89a 100644 --- a/app/models/billing/service.rb +++ b/app/models/billing/service.rb @@ -77,7 +77,7 @@ class Billing::Service < ApplicationRecord after_destroy :provisioning_object_after_destroy scope :ready_for_renew, lambda { - where('renew_period_id is not null AND renew_at <= ? ', Time.current) + where('renew_period_id is not null AND renew_at <= ? ', Time.current).order(renew_at: :asc) } scope :one_time_services, lambda { where('renew_period_id is null') diff --git a/app/models/billing/service/renew.rb b/app/models/billing/service/renew.rb index 822460ece..6dd41e956 100644 --- a/app/models/billing/service/renew.rb +++ b/app/models/billing/service/renew.rb @@ -4,6 +4,12 @@ class Billing::Service::Renew Error = Class.new(StandardError) DESCRIPTION = 'Renew service' + class << self + def perform(service) + new(service).perform + end + end + attr_reader :service delegate :account, to: :service @@ -20,30 +26,32 @@ def perform service.update!(state_id: Billing::Service::STATE_ID_SUSPENDED) provisioning_object.after_failed_renew provisioning_object.after_renew - return + Rails.logger.info { "Not enough balance to renew billing service ##{service.id}" } + else + service.update!(state_id: Billing::Service::STATE_ID_ACTIVE, renew_at: next_renew_at) + transaction = create_transaction + provisioning_object.after_success_renew + provisioning_object.after_renew + transaction + Rails.logger.info { "Success renew billing service ##{service.id}" } end - - service.update!( - state_id: Billing::Service::STATE_ID_ACTIVE, - renew_at: next_renew_at - ) - - transaction = Billing::Transaction.new( - service:, - account:, - amount: service.renew_price, - description: DESCRIPTION - ) - raise Error, "Failed to create transaction: #{transaction.errors.full_messages.to_sentence}" unless transaction.save - - provisioning_object.after_success_renew - provisioning_object.after_renew - transaction end end private + def create_transaction + transaction = Billing::Transaction.new( + service:, + account:, + amount: service.renew_price, + description: DESCRIPTION + ) + raise Error, "Failed to create transaction: #{transaction.errors.full_messages.to_sentence}" unless transaction.save + + transaction + end + def provisioning_object @provisioning_object ||= service.build_provisioning_object end diff --git a/spec/jobs/jobs/service_renew_spec.rb b/spec/jobs/jobs/service_renew_spec.rb new file mode 100644 index 000000000..6df808f91 --- /dev/null +++ b/spec/jobs/jobs/service_renew_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe Jobs::ServiceRenew, '#call' do + subject do + job.call + end + + let(:job) { described_class.new(double) } + before do + create(:service, renew_period_id: nil, renew_at: 1.day.ago) + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_DAY, renew_at: nil) + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_DAY, renew_at: 1.minute.from_now) + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_MONTH, renew_at: 1.day.from_now) + end + + let!(:services_for_renew) do + [ + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_DAY, renew_at: 1.second.ago), + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_MONTH, renew_at: 1.day.ago), + create(:service, renew_period_id: Billing::Service::RENEW_PERIOD_ID_MONTH, renew_at: 1.month.ago) + ] + end + + it 'renews correct services' do + services_for_renew.each do |service| + expect(Billing::Service::Renew).to receive(:perform).with(service).once + end + subject + end + + context 'when renew raises an error' do + it 'renews all ready services' do + expect(Billing::Service::Renew).to receive(:perform).with(services_for_renew[0]).once.and_raise(StandardError, 'test0') + expect(Billing::Service::Renew).to receive(:perform).with(services_for_renew[1]).once + expect(Billing::Service::Renew).to receive(:perform).with(services_for_renew[2]).once.and_raise(StandardError, 'test2') + + expect(CaptureError).to receive(:capture).with( + a_kind_of(StandardError), + hash_including(extra: { service_id: services_for_renew[0].id }) + ).once + expect(CaptureError).to receive(:capture).with( + a_kind_of(StandardError), + hash_including(extra: { service_id: services_for_renew[2].id }) + ).once + + subject + end + end +end diff --git a/spec/models/billing/service/renew_spec.rb b/spec/models/billing/service/renew_spec.rb new file mode 100644 index 000000000..d8a6535ab --- /dev/null +++ b/spec/models/billing/service/renew_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +RSpec.describe Billing::Service::Renew do + shared_examples :renews_service do + it 'calls provisioning object callbacks' do + stub = instance_double(Billing::Provisioning::Base) + expect(service).to receive(:build_provisioning_object).and_return(stub) + stub + expect(stub).to receive(:before_renew).with(no_args).once.ordered + expect(stub).to receive(:after_success_renew).with(no_args).once.ordered + expect(stub).to receive(:after_renew).with(no_args).once.ordered + + subject + end + + it 'renews service' do + expect { subject }.to change { service.reload.renew_at }.by(1.day) + expect(service.state_id).to eq(Billing::Service::STATE_ID_ACTIVE) + end + + it 'charges account' do + expect { subject }.to change { account.reload.balance }.by(-service.renew_price) + end + + it 'creates Billing::Transaction' do + expect { subject }.to change { Billing::Transaction.count }.by(1) + transaction = Billing::Transaction.last! + expect(transaction).to have_attributes( + service:, + account:, + amount: service.renew_price, + description: described_class::DESCRIPTION + ) + end + end + + shared_examples :suspends_service do + it 'calls provisioning object callbacks' do + stub = instance_double(Billing::Provisioning::Base) + expect(service).to receive(:build_provisioning_object).and_return(stub) + stub + expect(stub).to receive(:before_renew).with(no_args).once.ordered + expect(stub).to receive(:after_failed_renew).with(no_args).once.ordered + expect(stub).to receive(:after_renew).with(no_args).once.ordered + + subject + end + + it 'suspends service' do + expect { subject }.not_to change { service.reload.renew_at } + expect(service.state_id).to eq(Billing::Service::STATE_ID_SUSPENDED) + end + + it 'does not charge account' do + expect { subject }.not_to change { account.reload.balance } + end + + it 'does not create Billing::Transaction' do + expect { subject }.to change { Billing::Transaction.count }.by(0) + end + end + + describe '.perform' do + subject do + described_class.perform(service) + end + + let!(:service_type) do + create(:service_type, service_type_attrs) + end + let(:service_type_attrs) do + {} + end + let!(:account) { create(:account, account_attrs) } + let!(:account_attrs) do + { balance: 100, min_balance: 0, max_balance: 1000 } + end + let!(:service) { create(:service, service_attrs) } + let!(:service_attrs) do + { + account:, + type: service_type, + renew_price: 10, + initial_price: 0, + renew_period_id: Billing::Service::RENEW_PERIOD_ID_DAY, + renew_at: Time.current.beginning_of_day + } + end + + context 'when enough money' do + include_examples :renews_service + + context 'when account balance will be less than min_balance' do + let!(:account_attrs) do + super().merge min_balance: 90.01 + end + + include_examples :suspends_service + end + end + + context 'when not enough money' do + let!(:account_attrs) do + super().merge balance: 9.99 + end + + context 'when service_type with force_renew' do + let(:service_type_attrs) do + super().merge force_renew: true + end + + include_examples :renews_service + end + end + end +end