Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/admin/billing/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
menu parent: 'Billing', label: 'Services', priority: 30

controller do
def update(*)
if resource.terminated?
flash[:error] = 'Service has been terminated and cannot be updated.'
redirect_to resource_path(resource)
return
end

super
end

def create_resource(object)
object.save
rescue Billing::Provisioning::Errors::Error => e
Expand Down Expand Up @@ -115,6 +125,25 @@ def destroy_resource(object)
end
end

action_item :terminate, only: %i[show edit] do
if authorized?(:terminate, resource)
link_to 'Terminate',
terminate_service_path(resource),
method: :post,
data: { confirm: 'Are you sure you want to terminate this service?' }
end
end

member_action :terminate, method: :post do
Billing::Service::Terminate.call(record: resource)

flash[:notice] = 'Service has been terminated.'
redirect_to resource_path(resource)
rescue Billing::Service::Terminate::Error => e
flash[:error] = e.message
redirect_to resource_path(resource)
end

permit_params :name, :account_id, :type_id, :variables_json, :initial_price, :renew_price, :renew_at, :renew_period_id

form do |f|
Expand Down
11 changes: 10 additions & 1 deletion app/decorators/service_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ def state_badge
private

def state_color
object.state_id == Billing::Service::STATE_ID_ACTIVE ? 'ok' : 'red'
case object.state_id
when Billing::Service::STATE_ID_ACTIVE
'ok'
when Billing::Service::STATE_ID_SUSPENDED
'warning'
when Billing::Service::STATE_ID_TERMINATED
'error'
else
'warning'
end
end
end
19 changes: 17 additions & 2 deletions app/models/billing/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ class Billing::Service < ApplicationRecord

STATE_ID_ACTIVE = 10
STATE_ID_SUSPENDED = 20
STATE_ID_TERMINATED = 30

STATES = {
STATE_ID_ACTIVE => 'Active',
STATE_ID_SUSPENDED => 'Suspended'
STATE_ID_SUSPENDED => 'Suspended',
STATE_ID_TERMINATED => 'Terminated'
}.freeze

RENEW_PERIOD_ID_DAY = 10
Expand Down Expand Up @@ -69,6 +71,7 @@ class Billing::Service < ApplicationRecord
validates :renew_at, presence: true, allow_nil: false, if: proc { !renew_period_id.nil? }
validates :renew_at, absence: true, allow_nil: true, if: proc { renew_period_id.nil? }
validate :validate_variables
validate :prevent_state_change_from_terminated, on: :update

before_create :verify_provisioning_variables
before_create :assign_uuid
Expand All @@ -80,7 +83,9 @@ 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).order(renew_at: :asc)
where('renew_period_id is not null AND renew_at <= ? ', Time.current)
.where.not(state_id: STATE_ID_TERMINATED)
.order(renew_at: :asc)
}
scope :one_time_services, lambda {
where('renew_period_id is null')
Expand All @@ -94,6 +99,10 @@ def state
STATES[state_id]
end

def terminated?
state_id == STATE_ID_TERMINATED
end

def renew_period
renew_period_id.nil? ? RENEW_PERIOD_EMPTY : RENEW_PERIODS[renew_period_id]
end
Expand Down Expand Up @@ -137,6 +146,12 @@ def validate_variables
end
end

def prevent_state_change_from_terminated
return unless state_id_changed? && state_id_was == STATE_ID_TERMINATED

errors.add(:state_id, 'cannot be changed once terminated')
end

def verify_provisioning_variables
self.variables = build_provisioning_object.verify_service_variables!
rescue Billing::Provisioning::Errors::InvalidVariablesError => e
Expand Down
8 changes: 8 additions & 0 deletions app/models/billing/service/renew.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ def initialize(service)
end

def perform
return skip_terminated_service if service.terminated?

service.transaction do
account.lock! # will generate SELECT FOR UPDATE SQL statement
service.lock!
provisioning_object.before_renew

if !enough_balance? && !service.type.force_renew
Expand Down Expand Up @@ -56,6 +59,11 @@ def provisioning_object
@provisioning_object ||= service.build_provisioning_object
end

def skip_terminated_service
Rails.logger.info { "Skip renew billing service ##{service.id} because it is terminated" }
nil
end

def next_renew_at
if service.renew_period_id == Billing::Service::RENEW_PERIOD_ID_DAY
Time.current.beginning_of_day + 1.day
Expand Down
13 changes: 13 additions & 0 deletions app/policies/billing/service_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
module Billing
class ServicePolicy < ::RolePolicy
section 'Billing/Service'

def update?
allowed_for_role?(:change)
end

def edit?
allowed_for_role?(:change) && !record.terminated?
end

def terminate?
allowed_for_role?(:change) && !record.terminated?
end

class Scope < ::RolePolicy::Scope
end
end
Expand Down
27 changes: 27 additions & 0 deletions app/services/billing/service/terminate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Billing
class Service
class Terminate < ApplicationService
parameter :record, required: true

Error = Class.new(StandardError)

def call
raise_if_invalid!

record.transaction do
record.update!(state_id: Billing::Service::STATE_ID_TERMINATED)
end
rescue ActiveRecord::RecordNotSaved => e
raise Error, e.message
end

private

def raise_if_invalid!
raise Error, 'Service is already terminated.' if record.terminated?
end
end
end
end
4 changes: 4 additions & 0 deletions spec/factories/billing/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@
renew_at { 1.month.from_now.beginning_of_month }
renew_period_id { Billing::Service::RENEW_PERIOD_ID_MONTH }
end

trait :terminated do
state_id { Billing::Service::STATE_ID_TERMINATED }
end
end
end
2 changes: 1 addition & 1 deletion spec/features/billing/services/destroy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
let(:record_attrs) { { name: 'test', account:, type: service_type } }

describe 'from show page', :js do
subject { click_action_item 'Delete Service' }
subject { accept_confirm { click_action_item 'Delete Service' } }

before { visit service_path(record) }

Expand Down
42 changes: 42 additions & 0 deletions spec/features/billing/services/edit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,46 @@
expect(page).to have_field 'Renew price', with: ''
end
end

context 'when service is terminated' do
let(:record_attrs) { { state_id: Billing::Service::STATE_ID_TERMINATED } }

it 'redirects and blocks editing' do
visit edit_service_path(record.id)

expect(page).to have_flash_message(
'You are not authorized to perform this action.',
type: :error,
exact: true
)
expect(page).to have_current_path root_path
end
end

context 'when service is terminated after edit page is already opened', js: false do
before do
policy_roles = Rails.configuration.policy_roles.deep_merge(
user: {
Dashboard: { read: false }
}
)
allow(Rails.configuration).to receive(:policy_roles).and_return(policy_roles)
end

it 'redirects to service page without redirect loop' do
visit edit_service_path(record.id)
fill_in 'Name', with: attributes[:name]
Billing::Service::Terminate.call(record:)

click_submit('Update Service')

expect(page).to have_current_path service_path(record.id)
expect(page).to have_flash_message(
'Service has been terminated and cannot be updated.',
type: :error,
exact: true
)
expect(record.reload.name).not_to eq(attributes[:name])
end
end
end
21 changes: 21 additions & 0 deletions spec/features/billing/services/terminate_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

RSpec.describe 'Billing Services Terminate', :js do
include_context :login_as_admin

let!(:account) { create(:account) }
let!(:service_type) { create(:service_type) }
let!(:record) { create(:service, :renew_daily, account:, type: service_type) }

before do
visit service_path(record.id)
end

it 'terminates service from show page' do
accept_confirm { click_action_item 'Terminate' }

expect(page).to have_flash_message('Service has been terminated.', type: :notice, exact: true)
expect(record.reload.state_id).to eq(Billing::Service::STATE_ID_TERMINATED)
expect(page).not_to have_action_item('Edit Services')
end
end
19 changes: 19 additions & 0 deletions spec/models/billing/service/renew_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,24 @@
end
end
end

context 'when service is terminated' do
let(:service_attrs) do
super().merge(state_id: Billing::Service::STATE_ID_TERMINATED)
end

it 'does not renew' do
expect(service).not_to receive(:build_provisioning_object)

expect { subject }.not_to change {
[
service.reload.renew_at,
service.reload.state_id,
Billing::Transaction.count,
account.reload.balance
]
}
end
end
end
end