Skip to content

Commit

Permalink
Merge pull request #1505 from senid231/provisioning-object-validation
Browse files Browse the repository at this point in the history
provisioning object validation
  • Loading branch information
dmitry-sinina authored Jul 15, 2024
2 parents 9bc8069 + a4f15c6 commit 87303ef
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 12 deletions.
29 changes: 29 additions & 0 deletions app/models/billing/provisioning/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
module Billing
module Provisioning
class Base
class << self
# @param service_type [Billing::ServiceType]
# @raise [Billing::Provisioning::Errors::InvalidVariablesError]
# @return [Hash,nil] verified service_type variables
def verify_service_type_variables!(service_type)
service_type.variables
end
end

attr_reader :service
delegate :account, to: :service

Expand All @@ -11,6 +20,12 @@ def initialize(service)
@service = service
end

# @raise [Billing::Provisioning::Errors::InvalidVariablesError]
# @return [Hash,nil] verified service variables
def verify_service_variables!
service.variables
end

def after_create
nil
end
Expand All @@ -34,6 +49,20 @@ def after_success_renew
def after_failed_renew
nil
end

# Called before destroying the service and before destroying dependant package_counters
# To prevent service destruction:
# service.errors.add(:base, ...)
# throw(:abort)
#
def before_destroy
nil
end

# Called after destroying the service but before transaction COMMIT
def after_destroy
nil
end
end
end
end
49 changes: 49 additions & 0 deletions app/models/billing/provisioning/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Billing
module Provisioning
module Errors
class Error < StandardError
end

class InvalidVariablesError < Error
attr_reader :errors

# @param errors [Hash,String]
# @param msg [String,nil]
def initialize(errors, msg = nil)
if errors.is_a?(String)
msg = errors
errors = { base: [msg] }
end
@errors = errors
super(msg || "Validation error: #{full_error_messages.join(', ')}")
end

def full_error_messages(errors = @errors, prefix = nil)
full_messages = []
errors.each do |key, values|
if values.is_a?(Array)
full_messages.concat parse_errors_array(key, values, prefix)
elsif values.is_a?(Hash)
full_messages.concat full_error_messages(values, [prefix, key].compact.join('.'))
else
raise ArgumentError, "Invalid error format: #{values.inspect}\nerrors: #{errors.inspect}"
end
end
full_messages
end

def parse_errors_array(key, values, prefix)
values.map do |value|
if prefix.nil?
key == :base || key.nil? ? value : "#{prefix}.#{key} - #{value}"
else
key == :base || key.nil? ? "#{prefix} - #{value}" : "#{prefix}.#{key} - #{value}"
end
end
end
end
end
end
end
55 changes: 47 additions & 8 deletions app/models/billing/provisioning/free_minutes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,60 @@ module Provisioning
# "ignore_prefixes" is optional and defaults to []
#
class FreeMinutes < Base
def after_create
raise ArgumentError, 'No prefixes configured' if prefixes.empty?
PrefixSchema = Dry::Schema.JSON do
required(:prefix).filled(:string)
required(:duration).value(:integer)
optional(:exclude).maybe(:bool)
end

class ServiceTypeVariablesContract < Dry::Validation::Contract
json do
optional(:prefixes).array(PrefixSchema).default([])
end
end

class ServiceVariablesContract < Dry::Validation::Contract
json do
optional(:prefixes).array(PrefixSchema).default([])
optional(:ignore_prefixes).array(:string).default([])
end
end

class << self
def verify_service_type_variables!(service_type)
contract = ServiceTypeVariablesContract.new
result = contract.call(service_type.variables || {})
raise Billing::Provisioning::Errors::InvalidVariablesError, result.errors.to_h unless result.success?

result.to_h
end
end

def verify_service_variables!
contract = ServiceVariablesContract.new
result = contract.call(service.variables || {})
raise Billing::Provisioning::Errors::InvalidVariablesError, result.errors.to_h unless result.success?

result.to_h
end

def after_create
prefixes_data.each do |data|
create_or_reset_counter(data)
end
end

def after_success_renew
raise ArgumentError, 'No prefixes configured' if prefixes.empty?

prefixes_data.each do |data|
create_or_reset_counter(data)
end
destroy_obsolete_counters
end

def before_destroy
nil
end

private

# if counter for prefix does not exist - will created it.
Expand Down Expand Up @@ -73,17 +110,19 @@ def prefixes_data

def build_prefixes_data
# service may override some service.type prefixes
ignore_type_prefixes = service.variables['prefixes'].map { |data| data['prefix'] }
service_prefixes = service.variables['prefixes']
service_type_prefixes = service.type.variables['prefixes']
ignore_type_prefixes = service_prefixes.map { |data| data['prefix'] }
# service may ignore some service.type prefixes
ignore_type_prefixes += service.variables['ignore_prefixes'] || []
ignore_type_prefixes += service.variables['ignore_prefixes']

data_list = []
service.type.variables['prefixes'].each do |data|
service_type_prefixes.each do |data|
next if ignore_type_prefixes.include? data['prefix']

data_list << data
end
service.variables['prefixes'].each do |data|
service_prefixes.each do |data|
data_list << data
end
data_list.map do |data|
Expand Down
20 changes: 20 additions & 0 deletions app/models/billing/provisioning/logging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
module Billing
module Provisioning
class Logging < Base
class << self
def verify_service_type_variables!(service_type)
Rails.logger.info "Verify service type variables variables=#{service_type.variables}"
super
end
end

def verify_service_variables!
Rails.logger.info "Verify service variables variables=#{service.variables}"
super
end

def after_create
Rails.logger.info "Service created service_id=#{service.id}"
end
Expand All @@ -22,6 +34,14 @@ def after_success_renew
def after_failed_renew
Rails.logger.info "Service failed to renew service_id=#{service.id}, account_balance=#{account.balance}"
end

def before_destroy
Rails.logger.info "Before destroy service service_id=#{service.id}"
end

def after_destroy
Rails.logger.info "After destroy service service_id=#{service.id}"
end
end
end
end
29 changes: 28 additions & 1 deletion app/models/billing/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,29 @@ class Billing::Service < ApplicationRecord
belongs_to :type, class_name: 'Billing::ServiceType'
belongs_to :account, class_name: 'Account'
has_many :transactions, class_name: 'Billing::Transaction'

# callback defined before association because it should be called before it's `dependent: :destroy` callback
before_destroy :provisioning_object_before_destroy
has_many :package_counters, class_name: 'Billing::PackageCounter', dependent: :destroy

attr_readonly :account_id, :type_id

before_validation { self.variables = nil if variables.blank? }

validates :initial_price, :renew_price, presence: true
validates :initial_price, :renew_price, numericality: true, allow_nil: true
validates :state_id, inclusion: { in: STATES.keys }
validates :renew_period_id, inclusion: { in: RENEW_PERIODS.keys }, allow_nil: true
validate :validate_variables

before_create :verify_provisioning_variables
before_update :verify_provisioning_variables, if: :variables_changed?

after_create :create_initial_transaction
after_create :provisioning_object_after_create

after_destroy :provisioning_object_after_destroy

scope :ready_for_renew, lambda {
where('renew_period_id is not null AND renew_at <= ? ', Time.current)
}
Expand Down Expand Up @@ -110,8 +120,25 @@ def provisioning_object_after_create
build_provisioning_object.after_create
end

def provisioning_object_before_destroy
build_provisioning_object.before_destroy
end

def provisioning_object_after_destroy
build_provisioning_object.after_destroy
end

def validate_variables
errors.add(:variables, 'must be a JSON object or empty') if !variables.nil? && !variables.is_a?(Hash)
if !variables.nil? && !variables.is_a?(Hash)
errors.add(:variables, 'must be a JSON object or empty')
end
end

def verify_provisioning_variables
self.variables = build_provisioning_object.verify_service_variables!
rescue Billing::Provisioning::Errors::InvalidVariablesError => e
e.full_error_messages.each { |msg| errors.add(:variables, msg) }
throw(:abort)
end

def create_initial_transaction
Expand Down
17 changes: 16 additions & 1 deletion app/models/billing/service_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ class Billing::ServiceType < ApplicationRecord

has_many :services, class_name: 'Billing::Service', foreign_key: :type_id, dependent: :restrict_with_error

before_validation { self.variables = nil if variables.blank? }

validates :name, presence: true
validates :name, uniqueness: true, allow_blank: true
validates :provisioning_class, presence: true
validate :validate_provisioning_class
validates :force_renew, inclusion: { in: [true, false] }
validate :validate_variables

before_create :verify_provisioning_variables
before_update :verify_provisioning_variables, if: proc { variables_changed? || provisioning_class_changed? }

def display_name
name
end
Expand Down Expand Up @@ -64,6 +69,16 @@ def validate_provisioning_class
end

def validate_variables
errors.add(:variables, 'must be a JSON object or empty') if !variables.nil? && !variables.is_a?(Hash)
if !variables.nil? && !variables.is_a?(Hash)
errors.add(:variables, 'must be a JSON object or empty')
end
end

def verify_provisioning_variables
klass = provisioning_class.constantize
self.variables = klass.verify_service_type_variables!(self)
rescue Billing::Provisioning::Errors::InvalidVariablesError => e
e.full_error_messages.each { |msg| errors.add(:variables, msg) }
throw(:abort)
end
end
11 changes: 11 additions & 0 deletions config/initializers/dry_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require 'dry/schema'

class Dry::Schema::Macros::DSL
def default(value)
schema_dsl.before(:rule_applier) do |result|
result.update(name => value) unless result[name]
end
end
end
Loading

0 comments on commit 87303ef

Please sign in to comment.