internetee-registry/app/models/domain.rb
oleghasjanov 3c169bb00b Make admin contacts optional for private registrants
This change makes admin contacts optional for private registrants while keeping them mandatory for organizations. The changes include:

- Updated Domain model validations to make admin and tech contacts optional (min=0) for private registrants
- Added validation rules methods to handle different requirements based on registrant type
- Modified EPP domain creation to support domains without admin contacts for private registrants
- Updated attach_default_contacts to skip adding contacts for private registrants
- Added comprehensive test coverage for:
  - Domain model validations with private/org registrants
  - EPP domain creation without admin contacts for private registrants
  - REPP API contact management for private registrants

This implements the requirement to make admin contacts optional for private registrations of .ee domains while maintaining the existing validation rules for organizations.
2025-01-07 12:24:57 +02:00

866 lines
26 KiB
Ruby

class Domain < ApplicationRecord
include UserEvents
include Roids
include Versions # version/domain_version.rb
include Domain::Expirable
include Domain::Activatable
include Domain::ForceDelete
include Domain::Discardable
include Domain::Deletable
include Domain::Transferable
include Domain::RegistryLockable
include Domain::Releasable
include Domain::Disputable
include Domain::BulkUpdatable
PERIODS = [
['3 months', '3m'],
['6 months', '6m'],
['9 months', '9m'],
['1 year', '1y'],
['2 years', '2y'],
['3 years', '3y'],
['4 years', '4y'],
['5 years', '5y'],
['6 years', '6y'],
['7 years', '7y'],
['8 years', '8y'],
['9 years', '9y'],
['10 years', '10y'],
].freeze
attr_accessor :roles,
:legal_document_id,
:is_admin,
:registrant_typeahead,
:update_me,
:epp_pending_update,
:epp_pending_delete,
:reserved_pw
attr_accessor :skip_multiyears_expiration_email_validation
alias_attribute :on_hold_time, :outzone_at
alias_attribute :outzone_time, :outzone_at
alias_attribute :auth_info, :transfer_code # Old attribute name; for PaperTrail
alias_attribute :registered_at, :created_at
store_accessor :json_statuses_history,
:force_delete_domain_statuses_history,
:admin_store_statuses_history
# TODO: whois requests ip whitelist for full info for own domains and partial info for other domains
# TODO: most inputs should be trimmed before validation, probably some global logic?
belongs_to :registrar, required: true
belongs_to :registrant, required: true
# TODO: should we user validates_associated :registrant here?
has_many :admin_domain_contacts
accepts_nested_attributes_for :admin_domain_contacts,
allow_destroy: true, reject_if: :admin_change_prohibited?
has_many :tech_domain_contacts
accepts_nested_attributes_for :tech_domain_contacts,
allow_destroy: true, reject_if: :tech_change_prohibited?
def registrant_change_prohibited?
statuses.include? DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED
end
# NB! contacts, admin_contacts, tech_contacts are empty for a new record
has_many :domain_contacts, dependent: :destroy
has_many :contacts, through: :domain_contacts, source: :contact
has_many :admin_contacts, through: :admin_domain_contacts, source: :contact
has_many :tech_contacts, through: :tech_domain_contacts, source: :contact
has_many :nameservers, dependent: :destroy, inverse_of: :domain
accepts_nested_attributes_for :nameservers, allow_destroy: true,
reject_if: proc { |attrs| attrs[:hostname].blank? }
has_many :transfers, class_name: 'DomainTransfer', dependent: :destroy
has_many :dnskeys, dependent: :destroy
has_one :whois_record # destroyment will be done in after_commit
accepts_nested_attributes_for :dnskeys, allow_destroy: true
has_many :legal_documents, as: :documentable
has_many :registrant_verifications, dependent: :destroy
has_one :csync_record, dependent: :destroy
after_initialize do
self.pending_json = {} if pending_json.blank?
self.statuses = [] if statuses.nil?
self.status_notes = {} if status_notes.nil?
end
before_save :manage_automatic_statuses
before_save :touch_always_version
def touch_always_version
self.updated_at = Time.zone.now
end
before_update :manage_statuses
def manage_statuses
return unless will_save_change_to_registrant_id? # rollback has not yet happened
pending_update! if registrant_verification_asked?
true
end
after_commit :update_whois_record
after_create :update_reserved_domains
def update_reserved_domains
ReservedDomain.new_password_for(name) if in_reserved_list?
end
validates :name_dirty, domain_name: true, uniqueness: true
validates :puny_label, length: { maximum: 63 }
validates :period, presence: true, numericality: { only_integer: true }
validates :transfer_code, presence: true
validate :validate_reservation
def validate_reservation
return if persisted? || !in_reserved_list?
if reserved_pw.blank?
errors.add(:base, :required_parameter_missing_reserved)
return false
end
return if ReservedDomain.pw_for(name) == reserved_pw
errors.add(:base, :invalid_auth_information_reserved)
end
validate :status_is_consistant
def status_is_consistant
has_error = (hold_status? && statuses.include?(DomainStatus::SERVER_MANUAL_INZONE))
if !has_error && statuses.include?(DomainStatus::PENDING_DELETE)
has_error = statuses.include? DomainStatus::SERVER_DELETE_PROHIBITED
end
errors.add(:domains, I18n.t(:object_status_prohibits_operation)) if has_error
end
# Removed to comply new ForceDelete procedure
# at https://github.com/internetee/registry/issues/1428#issuecomment-570561967
#
# validate :check_permissions, :unless => :is_admin
# def check_permissions
# return unless force_delete_scheduled?
# errors.add(:base, I18n.t(:object_status_prohibits_operation))
# false
# end
validates :nameservers, domain_nameserver: {
min: -> { Setting.ns_min_count },
max: -> { Setting.ns_max_count },
}
validates :dnskeys, object_count: {
min: -> { Setting.dnskeys_min_count },
max: -> { Setting.dnskeys_max_count },
}
def self.admin_contacts_validation_rules(for_org:)
{
min: -> { for_org ? Setting.admin_contacts_min_count : 0 },
max: -> { Setting.admin_contacts_max_count }
}
end
def self.tech_contacts_validation_rules(for_org:)
{
min: -> { for_org ? Setting.tech_contacts_min_count : 0 },
max: -> { Setting.tech_contacts_max_count }
}
end
validates :admin_domain_contacts,
object_count: admin_contacts_validation_rules(for_org: true),
if: :require_admin_contacts?
validates :admin_domain_contacts,
object_count: admin_contacts_validation_rules(for_org: false),
unless: :require_admin_contacts?
validates :tech_domain_contacts,
object_count: tech_contacts_validation_rules(for_org: true),
if: :require_tech_contacts?
validates :tech_domain_contacts,
object_count: tech_contacts_validation_rules(for_org: false),
unless: :require_tech_contacts?
validates :nameservers, uniqueness_multi: {
attribute: 'hostname',
}
validates :dnskeys, uniqueness_multi: {
attribute: 'public_key',
}
validate :validate_nameserver_ips
validate :statuses_uniqueness
def security_level_resolver
resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
resolver.do_validation = true
resolver.do_caching = false
resolver.dnssec = true
resolver
end
def dnssec_security_level(stubber: nil)
Dnsruby::Dnssec.reset
resolver = security_level_resolver
Dnsruby::Recursor.clear_caches(resolver)
if Rails.env.staging?
clear_dnssec_trusted_anchors_and_keys
elsif stubber
Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
end
recursor = Dnsruby::Recursor.new(resolver)
recursor.dnssec = true
recursor.query(name, 'A', 'IN').security_level
end
def clear_dnssec_trusted_anchors_and_keys
Dnsruby::Dnssec.clear_trust_anchors
Dnsruby::Dnssec.clear_trusted_keys
Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
end
def statuses_uniqueness
return if statuses.uniq == statuses
errors.add(:statuses, :taken)
end
self.ignored_columns = %w[legacy_id legacy_registrar_id legacy_registrant_id]
def subordinate_nameservers
nameservers.select { |x| x.hostname.end_with?(name) }
end
def delegated_nameservers
nameservers.reject { |x| x.hostname.end_with?(name) }
end
def extension_update_prohibited?
statuses.include? DomainStatus::SERVER_EXTENSION_UPDATE_PROHIBITED
end
def dnskey_update_enabled?
statuses.include? DomainStatus::SERVER_OBJ_UPDATE_PROHIBITED
end
def admin_change_prohibited?
statuses.include? DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED
end
def tech_change_prohibited?
statuses.include? DomainStatus::SERVER_TECH_CHANGE_PROHIBITED
end
class << self
def ransackable_associations(*)
authorizable_ransackable_associations
end
def ransackable_attributes(*)
authorizable_ransackable_attributes
end
def nameserver_required?
Setting.nameserver_required
end
def registrant_user_admin_registrant_domains(registrant_user)
companies = Contact.registrant_user_company_contacts(registrant_user)
from(
"(#{registrant_user_administered_domains(registrant_user).to_sql} UNION " \
"#{registrant_user_company_registrant(companies).to_sql} UNION " \
"#{registrant_user_domains_company(companies, except_tech: true).to_sql}) AS domains"
)
end
def registrant_user_direct_admin_registrant_domains(registrant_user)
from(
"(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_direct_domains_by_contact(registrant_user,
except_tech: true).to_sql}) AS domains"
)
end
def registrant_user_domains(registrant_user)
from(
"(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_indirect_domains(registrant_user).to_sql} UNION " \
"#{registrant_user_domains_by_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_direct_domains(registrant_user)
from(
"(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_direct_domains_by_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_administered_domains(registrant_user)
from(
"(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
"#{registrant_user_domains_by_admin_contact(registrant_user).to_sql}) AS domains"
)
end
def registrant_user_indirect_domains(registrant_user)
companies = Contact.registrant_user_company_contacts(registrant_user)
from(
"(#{registrant_user_company_registrant(companies).to_sql} UNION "\
"#{registrant_user_domains_company(companies).to_sql}) AS domains"
)
end
private
def registrant_user_domains_by_registrant(registrant_user)
where(registrant: registrant_user.contacts)
end
def registrant_user_domains_by_contact(registrant_user)
joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts })
end
def registrant_user_domains_by_admin_contact(registrant_user)
joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts,
type: [AdminDomainContact.name] })
end
def registrant_user_direct_domains_by_registrant(registrant_user)
where(registrant: registrant_user.direct_contacts)
end
def registrant_user_direct_domains_by_contact(registrant_user, except_tech: false)
request = { contact_id: registrant_user.direct_contacts }
request[:type] = [AdminDomainContact.name] if except_tech
joins(:domain_contacts).where(domain_contacts: request)
end
def registrant_user_company_registrant(companies)
where(registrant: companies)
end
def registrant_user_domains_company(companies, except_tech: false)
request = { contact: companies }
request[:type] = [AdminDomainContact.name] if except_tech
joins(:domain_contacts).where(domain_contacts: request)
end
end
def name=(value)
value&.strip!
value&.downcase!
self[:name] = SimpleIDN.to_unicode(value)
self[:name_puny] = SimpleIDN.to_ascii(value)
self[:name_dirty] = value
end
# find by internationalized domain name
# internet domain name => ascii or puny, but db::domains.name is unicode
def self.find_by_idn(name)
domain = find_by(name: name)
if domain.blank? && name.include?('-')
unicode = SimpleIDN.to_unicode name # we have no index on domains.name_puny
domain = find_by(name: unicode)
end
domain
end
def puny_label
name_puny.to_s.split('.').first
end
def registrant_typeahead
@registrant_typeahead || registrant.try(:name) || nil
end
def in_reserved_list?
@in_reserved_list ||= ReservedDomain.by_domain(name).any?
end
def pending_transfer
transfers.find_by(status: DomainTransfer::PENDING)
end
def server_holdable?
return false if statuses.include?(DomainStatus::SERVER_HOLD)
return false if statuses.include?(DomainStatus::SERVER_MANUAL_INZONE)
true
end
def renewable?
return false unless renew_blocking_statuses.empty?
return true unless Setting.days_to_renew_domain_before_expire != 0
# if you can renew domain at days_to_renew before domain expiration
return false if (expire_time.to_date - Time.zone.today) + 1 > Setting.days_to_renew_domain_before_expire
true
end
def renew_blocking_statuses
disallowed = [DomainStatus::DELETE_CANDIDATE, DomainStatus::PENDING_RENEW,
DomainStatus::PENDING_TRANSFER, DomainStatus::CLIENT_RENEW_PROHIBITED,
DomainStatus::PENDING_UPDATE, DomainStatus::SERVER_RENEW_PROHIBITED,
DomainStatus::PENDING_DELETE_CONFIRMATION]
(statuses & disallowed)
end
def notify_registrar(message_key)
# TODO: To be deleted with DomainDeleteConfirm refactoring
registrar.notifications.create!(
text: "#{I18n.t(message_key)}: #{name}",
attached_obj_id: id,
attached_obj_type: self.class.to_s
)
end
def preclean_pendings
# TODO: To be deleted with refactoring
self.registrant_verification_token = nil
self.registrant_verification_asked_at = nil
end
def clean_pendings!
# TODO: To be deleted with refactoring
preclean_pendings
self.pending_json = {}
statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION)
statuses.delete(DomainStatus::PENDING_UPDATE)
statuses.delete(DomainStatus::PENDING_DELETE)
status_notes[DomainStatus::PENDING_UPDATE] = ''
status_notes[DomainStatus::PENDING_DELETE] = ''
save
end
def pending_update!
return true if pending_update?
self.epp_pending_update = true # for epp
return true unless registrant_verification_asked?
pending_json_cache = pending_json
token = registrant_verification_token
asked_at = registrant_verification_asked_at
new_registrant_id = registrant.id
new_registrant_email = registrant.email
new_registrant_name = registrant.name
send_time = Time.zone.now + 1.minute
RegistrantChangeConfirmEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)
RegistrantChangeNoticeEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)
reload
self.pending_json = pending_json_cache
self.registrant_verification_token = token
self.registrant_verification_asked_at = asked_at
set_pending_update
touch_always_version
pending_json['new_registrant_id'] = new_registrant_id
pending_json['new_registrant_email'] = new_registrant_email
pending_json['new_registrant_name'] = new_registrant_name
# This pending_update! method is triggered by before_update
# Note, all before_save callbacks are executed before before_update,
# thus automatic statuses has already executed by this point
# and we need to trigger automatic statuses manually (second time).
manage_automatic_statuses
end
def registrant_update_confirmable?(token)
return false if statuses.include? DomainStatus::DELETE_CANDIDATE
return false unless pending_update?
return false unless registrant_verification_asked?
return false unless registrant_verification_token == token
true
end
def registrant_delete_confirmable?(token)
return false unless pending_delete?
return false unless registrant_verification_asked?
return false unless registrant_verification_token == token
true
end
def registrant_verification_asked?
registrant_verification_asked_at.present? && registrant_verification_token.present?
end
def registrant_verification_asked!(frame_str, current_user_id)
pending_json['frame'] = frame_str
pending_json['current_user_id'] = current_user_id
self.registrant_verification_asked_at = Time.zone.now
self.registrant_verification_token = SecureRandom.hex(42)
end
def pending_delete!
return true if pending_delete?
self.epp_pending_delete = true # for epp
# TODO: if this were to ever return true, that would be wrong. EPP would report sucess pending
return true unless registrant_verification_asked?
pending_delete_confirmation!
save(validate: false) # should check if this did succeed
Domains::DeleteConfirmEmail::SendRequest.run(domain: self)
end
def cancel_pending_delete
statuses.delete DomainStatus::PENDING_DELETE_CONFIRMATION
statuses.delete DomainStatus::PENDING_DELETE
self.delete_date = nil
end
def pricelist(operation_category, period_i = nil, unit = nil)
period_i ||= period
unit ||= period_unit
zone_name = name.split('.').drop(1).join('.')
zone = DNS::Zone.find_by(origin: zone_name)
duration = "P#{period_i}#{unit.upcase}"
Billing::Price.price_for(zone, operation_category, duration)
end
### VALIDATIONS ###
def validate_nameserver_ips
nameservers.to_a.reject(&:marked_for_destruction?).each do |ns|
next unless ns.hostname.end_with?(".#{name}")
next if ns.ipv4.present? || ns.ipv6.present?
errors.add(:nameservers, :invalid) if errors[:nameservers].blank?
ns.errors.add(:ipv4, :blank)
end
end
# used for highlighting form tabs
def parent_valid?
assoc_errors = errors.keys.select { |x| x.match(/\./) }
(errors.keys - assoc_errors).empty?
end
## SHARED
def name_in_wire_format
res = ''
parts = name_puny.split('.')
parts.each do |x|
res += format('%02X', x.length) # length of label in hex
res += x.each_byte.map { |b| format('%02X', b) }.join # label
end
res += '00'
res
end
def to_s
name
end
def pending_registrant
return '' if pending_json.blank?
return '' if pending_json['new_registrant_id'].blank?
Registrant.find_by(id: pending_json['new_registrant_id'])
end
def pending_update?
statuses.include?(DomainStatus::PENDING_UPDATE)
end
# depricated not used, not valid
def update_prohibited?
(statuses & DomainStatus::UPDATE_PROHIBIT_STATES).present?
end
# public api
def delete_prohibited?
statuses.include?(DomainStatus::FORCE_DELETE)
end
def update_unless_locked_by_registrant(update)
update(admin_store_statuses_history: update) unless locked_by_registrant?
end
def update_not_by_locked_statuses(update)
return unless locked_by_registrant?
result = update.reject { |status| RegistryLockable::LOCK_STATUSES.include? status }
update(admin_store_statuses_history: result)
end
# special handling for admin changing status
def admin_status_update(update)
return unless update
PaperTrail.request(enabled: false) do
update_unless_locked_by_registrant(update)
update_not_by_locked_statuses(update)
end
# check for deleted status
statuses.each do |s|
next if update.include? s
case s
when DomainStatus::PENDING_DELETE
self.delete_date = nil
when DomainStatus::SERVER_MANUAL_INZONE # removal causes server hold to set
self.outzone_at = Time.zone.now if force_delete_scheduled?
when DomainStatus::EXPIRED # removal causes server hold to set
self.outzone_at = expire_time + 15.day
when DomainStatus::SERVER_HOLD # removal causes server hold to set
self.outzone_at = nil
end
end
end
def pending_update_prohibited?
(statuses_was & DomainStatus::UPDATE_PROHIBIT_STATES).present?
end
def set_pending_update
if pending_update_prohibited?
logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_UPDATE not allowed to set. [#{statuses}]"
return
end
statuses << DomainStatus::PENDING_UPDATE
end
def pending_delete?
(statuses & [DomainStatus::PENDING_DELETE_CONFIRMATION, DomainStatus::PENDING_DELETE]).any?
end
def pending_delete_confirmation?
statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION
end
def pending_delete_confirmation!
statuses << DomainStatus::PENDING_DELETE_CONFIRMATION unless pending_delete_prohibited?
end
def pending_delete_prohibited?
(statuses_was & DomainStatus::DELETE_PROHIBIT_STATES).present?
end
# let's use positive method names
def pending_deletable?
!pending_delete_prohibited?
end
def set_pending_delete
if pending_delete_prohibited?
logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_DELETE not allowed to set. [#{statuses}]"
return
end
statuses << DomainStatus::PENDING_DELETE
end
def set_server_hold
statuses << DomainStatus::SERVER_HOLD
self.outzone_at = Time.current
end
def manage_automatic_statuses
unless self.class.nameserver_required?
deactivate if nameservers.reject(&:marked_for_destruction?).empty?
activate if nameservers.reject(&:marked_for_destruction?).size >= Setting.ns_min_count
end
cancel_force_delete if force_delete_scheduled? && will_save_change_to_registrant_id?
if statuses.empty? && valid?
statuses << DomainStatus::OK
elsif (statuses.length > 1) || !valid?
statuses.delete(DomainStatus::OK)
statuses.delete(DomainStatus::EXPIRED) unless expired?
end
p_d = statuses.include?(DomainStatus::PENDING_DELETE)
s_h = (statuses & [DomainStatus::SERVER_MANUAL_INZONE, DomainStatus::SERVER_HOLD]).empty?
statuses << DomainStatus::SERVER_HOLD if p_d && s_h
end
def children_log
log = HashWithIndifferentAccess.new
log[:admin_contacts] = admin_contact_ids
log[:tech_contacts] = tech_contact_ids
log[:nameservers] = nameserver_ids
log[:dnskeys] = dnskey_ids
log[:legal_documents] = [legal_document_id]
log[:registrant] = [registrant_id]
log
end
def update_whois_record
UpdateWhoisRecordJob.set(wait: 1.minute).perform_later name, 'domain'
end
def status_notes_array=(notes)
self.status_notes = {}
notes ||= []
statuses.each_with_index do |status, i|
status_notes[status] = notes[i]
end
end
def primary_contact_emails
(admin_contacts.emails + [registrant.email]).uniq
end
def expired_domain_contact_emails
(primary_contact_emails +
["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
end
def all_related_emails
(admin_contacts.emails + tech_contacts.emails + [registrant.email]).uniq
end
def force_delete_contact_emails
(primary_contact_emails + tech_contacts.pluck(:email) +
["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
end
def prepared_domain_name
name.split('.')&.first
end
def new_registrant_email
pending_json['new_registrant_email']
end
def new_registrant_id
pending_json['new_registrant_id']
end
def as_json(_options)
hash = super
hash['auth_info'] = hash.delete('transfer_code') # API v1 requirement
hash['valid_from'] = hash['created_at'] # API v1 requirement
hash
end
def domain_name
DNS::DomainName.new(name)
end
def contact_emails_verification_failed
contacts.select(&:email_verification_failed?)&.map(&:email)&.uniq
end
def as_csv_row
[
name,
registrant_info[0],
registrant_info[1],
registrant_info[2],
registrant_info[3],
valid_to.to_formatted_s(:db),
registrar,
created_at.to_formatted_s(:db),
statuses,
admin_contacts.map { |c| "#{c.name}, #{c.code}, #{ApplicationController.helpers.ident_for(c)}" },
tech_contacts.map { |c| "#{c.name}, #{c.code}, #{ApplicationController.helpers.ident_for(c)}" },
nameservers.pluck(:hostname),
force_delete_date,
force_delete_data,
]
end
def as_pdf
domain_html = ApplicationController.render(template: 'domain/pdf', assigns: { domain: self })
generator = PDFKit.new(domain_html, { enable_local_file_access: true })
generator.to_pdf
end
def registrant_info
if registrant
return [registrant.name, registrant.ident, registrant.ident_country_code,
registrant.ident_type]
end
ver = Version::ContactVersion.where(item_id: registrant_id).last
contact = Contact.all_versions_for([registrant_id], created_at).first
contact = ObjectVersionsParser.new(ver).parse if contact.nil? && ver
[contact.try(:name), contact.try(:ident), contact.try(:ident_country_code),
contact.try(:ident_type)] || ['Deleted']
end
def registrant_ident_info
return ApplicationController.helpers.ident_for(registrant) if registrant
end
def self.csv_header
[
'Domain', 'Registrant name', 'Registrant ident', 'Registrant ident country code',
'Registrant ident type', 'Valid to', 'Registrar', 'Created at',
'Statuses', 'Admin. contacts', 'Tech. contacts', 'Nameservers', 'Force delete date',
'Force delete data'
]
end
def self.pdf(html)
kit = PDFKit.new(html)
kit.to_pdf
end
def self.expire_warning_period
Setting.expire_warning_period.days
end
def self.redemption_grace_period
Setting.redemption_grace_period.days
end
def self.outzone_candidates
where("#{attribute_alias(:outzone_time)} < ?", Time.zone.now)
end
def self.uses_zone?(zone)
exists?(['name ILIKE ?', "%.#{zone.origin}"])
end
def self.swap_elements(array, indexes)
indexes.each do |index|
array[index[0]], array[index[1]] = array[index[1]], array[index[0]]
end
array
end
def require_admin_contacts?
registrant.present? && registrant.org?
end
def require_tech_contacts?
registrant.present? && registrant.org?
end
end