internetee-registry/app/models/epp/domain.rb
Matt Farnsworth 51a5798914 Story #109367018 - add new model to handle domain values are changing, and need to be captured now, not later
New model will provide data to DomainMailer customized as needed for the invocation reason as passed via symbol to
send_mail(). The mail templetes then required modification to use the new data model. This should handle all the data
errors in the update process, including pendingUpdate, confirmed, rejected or expired.
2015-12-16 20:37:21 +02:00

888 lines
25 KiB
Ruby

# rubocop: disable Metrics/ClassLength
class Epp::Domain < Domain
include EppErrors
# TODO: remove this spagetti once data in production is correct.
attr_accessor :is_renewal
before_validation :manage_permissions
def manage_permissions
return unless update_prohibited? || delete_prohibited?
add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation))
false
end
after_validation :validate_contacts
def validate_contacts
return true if is_renewal
ok = true
active_admins = admin_domain_contacts.select { |x| !x.marked_for_destruction? }
active_techs = tech_domain_contacts.select { |x| !x.marked_for_destruction? }
# bullet workaround
ac = active_admins.map { |x| Contact.find(x.contact_id) }
tc = active_techs.map { |x| Contact.find(x.contact_id) }
# validate registrant here as well
([registrant] + ac + tc).each do |x|
unless x.valid?
add_epp_error('2304', nil, nil, I18n.t(:contact_is_not_valid, value: x.code))
ok = false
end
end
ok
end
before_save :link_contacts
def link_contacts
# Based on bullet report
if new_record?
# new record does not have correct instance contacts entries thanks to epp
unlinked_contacts = [registrant]
unlinked_contacts << admin_domain_contacts.map(&:contact)
unlinked_contacts << tech_domain_contacts.map(&:contact)
unlinked_contacts.flatten!
else
unlinked_contacts = contacts.select { |c| !c.linked? } # speed up a bit
end
unlinked_contacts.each do |uc|
uc.domains_present = true # no need to fetch domains again
uc.save(validate: false)
end
end
after_destroy :unlink_contacts
def unlink_contacts
contacts.each do |c|
c.domains_present = false
c.save(validate: false)
end
end
class << self
def new_from_epp(frame, current_user)
domain = Epp::Domain.new
domain.attributes = domain.attrs_from(frame, current_user)
domain.attach_default_contacts
domain
end
end
def epp_code_map # rubocop:disable Metrics/MethodLength
{
'2002' => [ # Command use error
[:base, :domain_already_belongs_to_the_querying_registrar]
],
'2003' => [ # Required parameter missing
[:registrant, :blank],
[:registrar, :blank],
[:base, :required_parameter_missing_reserved]
],
'2004' => [ # Parameter value range error
[:nameservers, :out_of_range,
{
min: Setting.ns_min_count,
max: Setting.ns_max_count
}
],
[:dnskeys, :out_of_range,
{
min: Setting.dnskeys_min_count,
max: Setting.dnskeys_max_count
}
],
[:admin_contacts, :out_of_range,
{
min: Setting.admin_contacts_min_count,
max: Setting.admin_contacts_max_count
}
],
[:tech_contacts, :out_of_range,
{
min: Setting.tech_contacts_min_count,
max: Setting.tech_contacts_max_count
}
]
],
'2005' => [ # Parameter value syntax error
[:name_dirty, :invalid, { obj: 'name', val: name_dirty }],
[:puny_label, :too_long, { obj: 'name', val: name_puny }]
],
'2201' => [ # Authorisation error
[:auth_info, :wrong_pw]
],
'2202' => [
[:base, :invalid_auth_information_reserved]
],
'2302' => [ # Object exists
[:name_dirty, :taken, { value: { obj: 'name', val: name_dirty } }],
[:name_dirty, :reserved, { value: { obj: 'name', val: name_dirty } }],
[:name_dirty, :blocked, { value: { obj: 'name', val: name_dirty } }]
],
'2304' => [ # Object status prohibits operation
[:base, :domain_status_prohibits_operation]
],
'2306' => [ # Parameter policy error
[:period, :out_of_range, { value: { obj: 'period', val: period } }],
[:base, :ds_data_with_key_not_allowed],
[:base, :ds_data_not_allowed],
[:base, :key_data_not_allowed],
[:period, :not_a_number],
[:period, :not_an_integer]
],
'2308' => [
[:base, :domain_name_blocked, { value: { obj: 'name', val: name_dirty } }]
]
}
end
def attach_default_contacts
return if registrant.blank?
regt = Registrant.find(registrant.id) # temp for bullet
tech_contacts << regt if tech_domain_contacts.blank?
admin_contacts << regt if admin_domain_contacts.blank? && !regt.org?
end
# rubocop: disable Metrics/PerceivedComplexity
# rubocop: disable Metrics/CyclomaticComplexity
# rubocop: disable Metrics/MethodLength
# rubocop: disable Metrics/AbcSize
def attrs_from(frame, current_user, action = nil)
at = {}.with_indifferent_access
code = frame.css('registrant').first.try(:text)
if code.present?
if action == 'chg' && registrant_change_prohibited?
add_epp_error('2304', nil, DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
end
regt = Registrant.find_by(code: code)
if regt
at[:registrant_id] = regt.id
else
add_epp_error('2303', 'registrant', code, [:registrant, :not_found])
end
end
at[:name] = frame.css('name').text if new_record?
at[:registrar_id] = current_user.registrar.try(:id)
at[:registered_at] = Time.zone.now if new_record?
period = frame.css('period').text
at[:period] = (period.to_i == 0) ? 1 : period.to_i
at[:period_unit] = Epp::Domain.parse_period_unit_from_frame(frame) || 'y'
at[:reserved_pw] = frame.css('reserved > pw').text
# at[:statuses] = domain_statuses_attrs(frame, action)
at[:nameservers_attributes] = nameservers_attrs(frame, action)
at[:admin_domain_contacts_attributes] = admin_domain_contacts_attrs(frame, action)
at[:tech_domain_contacts_attributes] = tech_domain_contacts_attrs(frame, action)
# at[:domain_statuses_attributes] = domain_statuses_attrs(frame, action)
pw = frame.css('authInfo > pw').text
at[:auth_info] = pw if pw.present?
if new_record?
dnskey_frame = frame.css('extension create')
else
dnskey_frame = frame
end
at[:dnskeys_attributes] = dnskeys_attrs(dnskey_frame, action)
at[:legal_documents_attributes] = legal_document_from(frame)
at
end
# rubocop: enable Metrics/PerceivedComplexity
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/MethodLength
# rubocop: enable Metrics/AbcSize
def nameservers_attrs(frame, action)
ns_list = nameservers_from(frame)
if action == 'rem'
to_destroy = []
ns_list.each do |ns_attrs|
nameserver = nameservers.find_by_hash_params(ns_attrs).first
if nameserver.blank?
add_epp_error('2303', 'hostAttr', ns_attrs[:hostname], [:nameservers, :not_found])
else
to_destroy << {
id: nameserver.id,
_destroy: 1
}
end
end
return to_destroy
else
return ns_list
end
end
def nameservers_from(frame)
res = []
frame.css('hostAttr').each do |x|
host_attr = {
hostname: x.css('hostName').first.try(:text),
ipv4: x.css('hostAddr[ip="v4"]').map(&:text).compact,
ipv6: x.css('hostAddr[ip="v6"]').map(&:text).compact
}
res << host_attr.delete_if { |_k, v| v.blank? }
end
res
end
def admin_domain_contacts_attrs(frame, action)
admin_attrs = domain_contact_attrs_from(frame, action, 'admin')
if admin_attrs.present? && admin_change_prohibited?
add_epp_error('2304', 'admin', DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
return []
end
case action
when 'rem'
return destroy_attrs(admin_attrs, admin_domain_contacts)
else
return admin_attrs
end
end
def tech_domain_contacts_attrs(frame, action)
tech_attrs = domain_contact_attrs_from(frame, action, 'tech')
if tech_attrs.present? && tech_change_prohibited?
add_epp_error('2304', 'tech', DomainStatus::SERVER_TECH_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
return []
end
case action
when 'rem'
return destroy_attrs(tech_attrs, tech_domain_contacts)
else
return tech_attrs
end
end
def destroy_attrs(attrs, dcontacts)
destroy_attrs = []
attrs.each do |at|
domain_contact_id = dcontacts.find_by(contact_id: at[:contact_id]).try(:id)
unless domain_contact_id
add_epp_error('2303', 'contact', at[:contact_code_cache], [:domain_contacts, :not_found])
next
end
destroy_attrs << {
id: domain_contact_id,
_destroy: 1
}
end
destroy_attrs
end
def domain_contact_attrs_from(frame, action, type)
attrs = []
frame.css('contact').each do |x|
next if x['type'] != type
c = Epp::Contact.find_by_epp_code(x.text)
unless c
add_epp_error('2303', 'contact', x.text, [:domain_contacts, :not_found])
next
end
if action != 'rem'
if x['type'] == 'admin' && c.org?
add_epp_error('2306', 'contact', x.text, [:domain_contacts, :admin_contact_can_be_only_private_person])
next
end
end
attrs << {
contact_id: c.id,
contact_code_cache: c.code
}
end
attrs
end
# rubocop: disable Metrics/PerceivedComplexity
# rubocop: disable Metrics/CyclomaticComplexity
def dnskeys_attrs(frame, action)
keys = []
return keys if frame.blank?
inf_data = DnsSecKeys.new(frame)
if action == 'rem' &&
frame.css('rem > all').first.try(:text) == 'true'
keys = inf_data.mark_destroy_all dnskeys
else
if Setting.key_data_allowed
errors.add(:base, :ds_data_not_allowed) if inf_data.ds_data.present?
keys = inf_data.key_data
end
if Setting.ds_data_allowed
errors.add(:base, :key_data_not_allowed) if inf_data.key_data.present?
keys = inf_data.ds_data
end
if action == 'rem'
keys = inf_data.mark_destroy(dnskeys)
add_epp_error('2303', nil, nil, [:dnskeys, :not_found]) if keys.include? nil
end
end
errors.any? ? [] : keys
end
# rubocop: enable Metrics/PerceivedComplexity
# rubocop: enable Metrics/CyclomaticComplexity
class DnsSecKeys
def initialize(frame)
@key_data = []
@ds_data = []
# schema validation prevents both in the same parent node
if frame.css('dsData').present?
ds_data_from frame
else
frame.css('keyData').each do |key|
@key_data.append key_data_from(key)
end
end
end
attr_reader :key_data
attr_reader :ds_data
def mark_destroy_all(dns_keys)
# if transition support required mark_destroy dns_keys when has ds/key values otherwise ...
dns_keys.map { |inf_data| mark inf_data }
end
def mark_destroy(dns_keys)
(ds_data.present? ? ds_filter(dns_keys) : kd_filter(dns_keys)).map do |inf_data|
inf_data.blank? ? nil : mark(inf_data)
end
end
private
KEY_INTERFACE = {flags: 'flags', protocol: 'protocol', alg: 'alg', public_key: 'pubKey' }
DS_INTERFACE =
{ ds_key_tag: 'keyTag',
ds_alg: 'alg',
ds_digest_type: 'digestType',
ds_digest: 'digest'
}
def xm_copy(frame, map)
result = {}
map.each do |key, elem|
result[key] = frame.css(elem).first.try(:text)
end
result
end
def key_data_from(frame)
xm_copy frame, KEY_INTERFACE
end
def ds_data_from(frame)
frame.css('dsData').each do |ds_data|
key = ds_data.css('keyData')
ds = xm_copy ds_data, DS_INTERFACE
ds.merge(key_data_from key) if key.present?
@ds_data << ds
end
end
def ds_filter(dns_keys)
@ds_data.map do |ds|
dns_keys.find_by(ds.slice(*DS_INTERFACE.keys))
end
end
def kd_filter(dns_keys)
@key_data.map do |key|
dns_keys.find_by(key)
end
end
def mark(inf_data)
{ id: inf_data.id, _destroy: 1 }
end
end
def domain_statuses_attrs(frame, action)
status_list = domain_status_list_from(frame)
if action == 'rem'
to_destroy = []
status_list.each do |x|
if statuses.include?(x)
to_destroy << x
else
add_epp_error('2303', 'status', x, [:domain_statuses, :not_found])
end
end
return to_destroy
else
return status_list
end
end
def domain_status_list_from(frame)
status_list = []
frame.css('status').each do |x|
unless DomainStatus::CLIENT_STATUSES.include?(x['s'])
add_epp_error('2303', 'status', x['s'], [:domain_statuses, :not_found])
next
end
status_list << x['s']
end
status_list
end
def legal_document_from(frame)
ld = frame.css('legalDocument').first
return [] unless ld
[{
body: ld.text,
document_type: ld['type']
}]
end
# rubocop: disable Metrics/AbcSize
# rubocop: disable Metrics/CyclomaticComplexity
def update(frame, current_user, verify = true)
return super if frame.blank?
at = {}.with_indifferent_access
at.deep_merge!(attrs_from(frame.css('chg'), current_user, 'chg'))
at.deep_merge!(attrs_from(frame.css('rem'), current_user, 'rem'))
if doc = attach_legal_document(Epp::Domain.parse_legal_document_from_frame(frame))
frame.css("legalDocument").first.content = doc.path if doc && doc.persisted?
end
at_add = attrs_from(frame.css('add'), current_user, 'add')
at[:nameservers_attributes] += at_add[:nameservers_attributes]
at[:admin_domain_contacts_attributes] += at_add[:admin_domain_contacts_attributes]
at[:tech_domain_contacts_attributes] += at_add[:tech_domain_contacts_attributes]
at[:dnskeys_attributes] += at_add[:dnskeys_attributes]
at[:statuses] =
statuses - domain_statuses_attrs(frame.css('rem'), 'rem') + domain_statuses_attrs(frame.css('add'), 'add')
# at[:statuses] += at_add[:domain_statuses_attributes]
if errors.empty? && verify &&
Setting.request_confrimation_on_registrant_change_enabled &&
frame.css('registrant').present? &&
frame.css('registrant').attr('verified').to_s.downcase != 'yes'
registrant_verification_asked!(frame.to_s, current_user.id)
end
self.deliver_emails = true # turn on email delivery for epp
errors.empty? && super(at)
end
# rubocop: enable Metrics/AbcSize
# rubocop: enable Metrics/CyclomaticComplexity
def apply_pending_update!
preclean_pendings
user = ApiUser.find(pending_json['current_user_id'])
frame = Nokogiri::XML(pending_json['frame'])
statuses.delete(DomainStatus::PENDING_UPDATE)
yield(self) if block_given? # need to skip statuses check here
self.save
::PaperTrail.whodunnit = user.id_role_username # updator str should be the request originator not the approval user
return unless update(frame, user, false)
clean_pendings!
self.deliver_emails = true # turn on email delivery
send_mail :registrant_updated_notification_for_old_registrant
send_mail :registrant_updated_notification_for_new_registrant
true
end
def apply_pending_delete!
preclean_pendings
statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION)
statuses.delete(DomainStatus::PENDING_DELETE)
DomainMailer.delete_confirmation(id, deliver_emails).deliver
clean_pendings!
set_pending_delete!
true
end
def attach_legal_document(legal_document_data)
return unless legal_document_data
legal_documents.create(
document_type: legal_document_data[:type],
body: legal_document_data[:body]
)
end
def epp_destroy(frame, user_id)
return false unless valid?
if doc = attach_legal_document(Epp::Domain.parse_legal_document_from_frame(frame))
frame.css("legalDocument").first.content = doc.path if doc && doc.persisted?
end
if Setting.request_confirmation_on_domain_deletion_enabled &&
frame.css('delete').children.css('delete').attr('verified').to_s.downcase != 'yes'
registrant_verification_asked!(frame.to_s, user_id)
self.deliver_emails = true # turn on email delivery for epp
pending_delete!
manage_automatic_statuses
true # aka 1001 pending_delete
else
set_pending_delete!
end
end
def set_pending_delete!
throw :epp_error, {
code: '2304',
msg: I18n.t(:object_status_prohibits_operation)
} unless pending_deletable?
self.delete_at = Time.zone.now + Setting.redemption_grace_period.days
set_pending_delete
set_server_hold if server_holdable?
save(validate: false)
end
### RENEW ###
def renew(cur_exp_date, period, unit = 'y')
@is_renewal = true
validate_exp_dates(cur_exp_date)
add_epp_error('2105', nil, nil, I18n.t('object_is_not_eligible_for_renewal')) unless renewable?
return false if errors.any?
p = self.class.convert_period_to_time(period, unit)
self.valid_to = valid_to + p
self.outzone_at = nil
self.delete_at = nil
self.period = period
self.period_unit = unit
statuses.delete(DomainStatus::SERVER_HOLD)
statuses.delete(DomainStatus::EXPIRED)
save
end
### TRANSFER ###
# rubocop: disable Metrics/CyclomaticComplexity
def transfer(frame, action, current_user)
case action
when 'query'
return domain_transfers.last if domain_transfers.any?
when 'request'
return pending_transfer if pending_transfer
return query_transfer(frame, current_user)
when 'approve'
return approve_transfer(frame, current_user) if pending_transfer
when 'reject'
return reject_transfer(frame, current_user) if pending_transfer
end
end
# TODO: Eager load problems here. Investigate how it's possible not to query contact again
# Check if versioning works with update_column
def transfer_contacts(registrar_id)
transfer_registrant(registrar_id)
transfer_domain_contacts(registrar_id)
end
def copy_and_transfer_contact(contact_id, registrar_id)
c = Contact.find(contact_id) # n+1 workaround
oc = c.deep_clone
oc.code = nil
oc.registrar_id = registrar_id
oc.copy_from_id = c.id
oc.prefix_code
oc.save!(validate: false)
oc
end
def transfer_registrant(registrar_id)
return if registrant.registrar_id == registrar_id
self.registrant_id = copy_and_transfer_contact(registrant_id, registrar_id).id
end
def transfer_domain_contacts(registrar_id)
copied_ids = []
contacts.each do |c|
next if copied_ids.include?(c.id) || c.registrar_id == registrar_id
if registrant_id_was == c.id # registrant was copied previously, do not copy it again
oc = OpenStruct.new(id: registrant_id)
else
oc = copy_and_transfer_contact(c.id, registrar_id)
end
domain_contacts.where(contact_id: c.id).update_all({ contact_id: oc.id }) # n+1 workaround
copied_ids << c.id
end
end
# rubocop: enable Metrics/PerceivedComplexity
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: disable Metrics/MethodLength
# rubocop: disable Metrics/AbcSize
def query_transfer(frame, current_user)
unless transferrable?
throw :epp_error, {
code: '2304',
msg: I18n.t(:object_status_prohibits_operation)
}
end
if current_user.registrar == registrar
throw :epp_error, {
code: '2002',
msg: I18n.t(:domain_already_belongs_to_the_querying_registrar)
}
end
old_contact_codes = contacts.pluck(:code).sort.uniq
old_registrant_code = registrant.code
transaction do
dt = domain_transfers.create!(
transfer_requested_at: Time.zone.now,
transfer_to: current_user.registrar,
transfer_from: registrar
)
if dt.pending?
registrar.messages.create!(
body: I18n.t('transfer_requested'),
attached_obj_id: dt.id,
attached_obj_type: dt.class.to_s
)
end
if dt.approved?
transfer_contacts(current_user.registrar_id)
dt.notify_losing_registrar(old_contact_codes, old_registrant_code)
generate_auth_info!
self.registrar = current_user.registrar
end
attach_legal_document(self.class.parse_legal_document_from_frame(frame))
save!(validate: false)
return dt
end
end
# rubocop: enable Metrics/AbcSize
# rubocop: enable Metrics/MethodLength
def approve_transfer(frame, current_user)
pt = pending_transfer
if current_user.registrar != pt.transfer_from
throw :epp_error, {
msg: I18n.t('transfer_can_be_approved_only_by_current_registrar'),
code: '2304'
}
end
transaction do
pt.update!(
status: DomainTransfer::CLIENT_APPROVED,
transferred_at: Time.zone.now
)
transfer_contacts(pt.transfer_to_id)
generate_auth_info
self.registrar = pt.transfer_to
attach_legal_document(self.class.parse_legal_document_from_frame(frame))
save!(validate: false)
end
pt
end
def reject_transfer(frame, current_user)
pt = pending_transfer
if current_user.registrar != pt.transfer_from
throw :epp_error, {
msg: I18n.t('transfer_can_be_rejected_only_by_current_registrar'),
code: '2304'
}
end
transaction do
pt.update!(
status: DomainTransfer::CLIENT_REJECTED
)
attach_legal_document(self.class.parse_legal_document_from_frame(frame))
save!(validate: false)
end
pt
end
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def keyrelay(parsed_frame, requester)
if registrar == requester
errors.add(:base, :domain_already_belongs_to_the_querying_registrar) and return false
end
abs_datetime = parsed_frame.css('absolute').text
abs_datetime = DateTime.zone.parse(abs_datetime) if abs_datetime.present?
transaction do
kr = keyrelays.build(
pa_date: Time.zone.now,
key_data_flags: parsed_frame.css('flags').text,
key_data_protocol: parsed_frame.css('protocol').text,
key_data_alg: parsed_frame.css('alg').text,
key_data_public_key: parsed_frame.css('pubKey').text,
auth_info_pw: parsed_frame.css('pw').text,
expiry_relative: parsed_frame.css('relative').text,
expiry_absolute: abs_datetime,
requester: requester,
accepter: registrar
)
legal_document_data = self.class.parse_legal_document_from_frame(parsed_frame)
if legal_document_data
kr.legal_documents.build(
document_type: legal_document_data[:type],
body: legal_document_data[:body]
)
end
kr.save
return false unless valid?
registrar.messages.create!(
body: 'Key Relay action completed successfully.',
attached_obj_type: kr.class.to_s,
attached_obj_id: kr.id
)
end
true
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
### VALIDATIONS ###
def validate_exp_dates(cur_exp_date)
begin
return if cur_exp_date.to_date == valid_to.to_date
rescue
add_epp_error('2306', 'curExpDate', cur_exp_date, I18n.t('errors.messages.epp_exp_dates_do_not_match'))
return
end
add_epp_error('2306', 'curExpDate', cur_exp_date, I18n.t('errors.messages.epp_exp_dates_do_not_match'))
end
### ABILITIES ###
def can_be_deleted?
begin
errors.add(:base, :domain_status_prohibits_operation)
return false
end if (statuses & [DomainStatus::CLIENT_DELETE_PROHIBITED, DomainStatus::SERVER_DELETE_PROHIBITED]).any?
true
end
def transferrable?
(statuses & [
DomainStatus::PENDING_DELETE_CONFIRMATION,
DomainStatus::PENDING_CREATE,
DomainStatus::PENDING_UPDATE,
DomainStatus::PENDING_DELETE,
DomainStatus::PENDING_RENEW,
DomainStatus::PENDING_TRANSFER,
DomainStatus::FORCE_DELETE,
DomainStatus::SERVER_TRANSFER_PROHIBITED,
DomainStatus::CLIENT_TRANSFER_PROHIBITED
]).empty?
end
## SHARED
# For domain transfer
def authenticate(pw)
errors.add(:auth_info, :wrong_pw) if pw != auth_info
errors.empty?
end
class << self
def parse_period_unit_from_frame(parsed_frame)
p = parsed_frame.css('period').first
return nil unless p
p[:unit]
end
def parse_legal_document_from_frame(parsed_frame)
ld = parsed_frame.css('legalDocument').first
return nil unless ld
return nil if ld.text.starts_with?(ENV['legal_documents_dir']) # escape reloading
{
body: ld.text,
type: ld['type']
}
end
def check_availability(domains)
domains = [domains] if domains.is_a?(String)
res = []
domains.each do |x|
x.strip!
x.downcase!
unless DomainNameValidator.validate_format(x)
res << { name: x, avail: 0, reason: 'invalid format' }
next
end
if ReservedDomain.pw_for(x).present?
res << { name: x, avail: 0, reason: I18n.t('errors.messages.epp_domain_reserved') }
next
end
if Domain.find_by_idn x
res << { name: x, avail: 0, reason: 'in use' }
else
res << { name: x, avail: 1 }
end
end
res
end
end
end
# rubocop: enable Metrics/ClassLength