mirror of
https://github.com/internetee/registry.git
synced 2025-06-07 13:15:40 +02:00
569 lines
17 KiB
Ruby
569 lines
17 KiB
Ruby
require 'deserializers/xml/legal_document'
|
|
|
|
class Contact < ApplicationRecord
|
|
include Versions # version/contact_version.rb
|
|
include Roids
|
|
include EppErrors
|
|
include UserEvents
|
|
include Contact::Transferable
|
|
include Contact::Identical
|
|
include Contact::Disclosable
|
|
include Contact::Archivable
|
|
include EmailVerifable
|
|
|
|
belongs_to :original, class_name: self.name
|
|
belongs_to :registrar, required: true
|
|
has_many :domain_contacts
|
|
has_many :domains, through: :domain_contacts
|
|
has_many :legal_documents, as: :documentable
|
|
has_many :registrant_domains, class_name: 'Domain', foreign_key: 'registrant_id'
|
|
has_many :actions, dependent: :destroy
|
|
|
|
attr_accessor :legal_document_id
|
|
alias_attribute :kind, :ident_type
|
|
alias_attribute :copy_from_id, :original_id # Old attribute name; for PaperTrail
|
|
|
|
scope :email_verification_failed, lambda {
|
|
joins('LEFT JOIN email_address_verifications emv ON contacts.email = emv.email')
|
|
.where('success = false and verified_at IS NOT NULL')
|
|
}
|
|
|
|
NAME_REGEXP = /([\u00A1-\u00B3\u00B5-\u00BF\u0021-\u0026\u0028-\u002C\u003A-\u0040]|
|
|
[\u005B-\u005F\u007B-\u007E\u2040-\u206F\u20A0-\u20BF\u2100-\u218F])/x.freeze
|
|
|
|
validates :name, :email, presence: true
|
|
validates :name, format: { without: NAME_REGEXP, message: :invalid }, if: -> { priv? }
|
|
|
|
validates :street, :city, :zip, :country_code, presence: true, if: lambda {
|
|
self.class.address_processing?
|
|
}
|
|
|
|
validates :phone, presence: true, e164: true, phone: true
|
|
|
|
validate :correct_email_format, if: proc { |c| c.will_save_change_to_email? }
|
|
|
|
validates :code,
|
|
uniqueness: { message: :epp_id_taken },
|
|
format: { with: /\A[\w\-\:\.\_]*\z/i, message: :invalid },
|
|
length: { maximum: 100, message: :too_long_contact_code }
|
|
validates_associated :identifier
|
|
|
|
validate :validate_html
|
|
validate :validate_country_code, if: -> { self.class.address_processing? }
|
|
|
|
after_initialize do
|
|
self.status_notes = {} if status_notes.nil?
|
|
self.ident_updated_at = Time.zone.now if new_record? && ident_updated_at.blank?
|
|
end
|
|
|
|
before_validation :to_upcase_country_code
|
|
before_validation :strip_email
|
|
|
|
composed_of :identifier,
|
|
class_name: 'Contact::Ident',
|
|
constructor: proc { |code, type, country_code| Contact::Ident.new(code: code,
|
|
type: type,
|
|
country_code: country_code) },
|
|
mapping: [%w[ident code], %w[ident_type type], %w[ident_country_code country_code]]
|
|
|
|
after_save :update_related_whois_records
|
|
before_validation :clear_address_modifications, if: -> { !self.class.address_processing? }
|
|
|
|
self.ignored_columns = %w[legacy_id legacy_history_id]
|
|
|
|
ORG = 'org'
|
|
PRIV = 'priv'
|
|
|
|
# For foreign private persons who has no national identification number
|
|
BIRTHDAY = 'birthday'.freeze
|
|
|
|
# From old registry software ("Fred"). No new contact can be created with this status
|
|
PASSPORT = 'passport'
|
|
|
|
#
|
|
# STATUSES
|
|
#
|
|
# Requests to delete the object MUST be rejected.
|
|
CLIENT_DELETE_PROHIBITED = 'clientDeleteProhibited'
|
|
SERVER_DELETE_PROHIBITED = 'serverDeleteProhibited'
|
|
|
|
# Requests to transfer the object MUST be rejected.
|
|
CLIENT_TRANSFER_PROHIBITED = 'clientTransferProhibited'
|
|
SERVER_TRANSFER_PROHIBITED = 'serverTransferProhibited'
|
|
|
|
# The contact object has at least one active association with
|
|
# another object, such as a domain object. Servers SHOULD provide
|
|
# services to determine existing object associations.
|
|
# "linked" status MAY be combined with any status.
|
|
LINKED = 'linked'
|
|
|
|
# This is the normal status value for an object that has no pending
|
|
# operations or prohibitions. This value is set and removed by the
|
|
# server as other status values are added or removed.
|
|
# "ok" status MAY only be combined with "linked" status.
|
|
OK = 'ok'
|
|
|
|
# Requests to update the object (other than to remove this status) MUST be rejected.
|
|
CLIENT_UPDATE_PROHIBITED = 'clientUpdateProhibited'
|
|
SERVER_UPDATE_PROHIBITED = 'serverUpdateProhibited'
|
|
|
|
# A transform command has been processed for the object, but the
|
|
# action has not been completed by the server. Server operators can
|
|
# delay action completion for a variety of reasons, such as to allow
|
|
# for human review or third-party action. A transform command that
|
|
# is processed, but whose requested action is pending, is noted with
|
|
# response code 1001.
|
|
# When the requested action has been completed, the pendingCreate,
|
|
# pendingDelete, pendingTransfer, or pendingUpdate status value MUST be
|
|
# removed. All clients involved in the transaction MUST be notified
|
|
# using a service message that the action has been completed and that
|
|
# the status of the object has changed.
|
|
# The pendingCreate, pendingDelete, pendingTransfer, and pendingUpdate
|
|
# status values MUST NOT be combined with each other.
|
|
PENDING_CREATE = 'pendingCreate'
|
|
# "pendingTransfer" status MUST NOT be combined with either
|
|
# "clientTransferProhibited" or "serverTransferProhibited" status.
|
|
PENDING_TRANSFER = 'pendingTransfer'
|
|
# "pendingUpdate" status MUST NOT be combined with either
|
|
# "clientUpdateProhibited" or "serverUpdateProhibited" status.
|
|
PENDING_UPDATE = 'pendingUpdate'
|
|
# "pendingDelete" MUST NOT be combined with either
|
|
# "clientDeleteProhibited" or "serverDeleteProhibited" status.
|
|
PENDING_DELETE = 'pendingDelete'
|
|
|
|
STATUSES = [
|
|
CLIENT_DELETE_PROHIBITED, SERVER_DELETE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED, CLIENT_UPDATE_PROHIBITED, SERVER_UPDATE_PROHIBITED,
|
|
OK, PENDING_CREATE, PENDING_DELETE, PENDING_TRANSFER,
|
|
PENDING_UPDATE, LINKED
|
|
]
|
|
|
|
CLIENT_STATUSES = [
|
|
CLIENT_DELETE_PROHIBITED, CLIENT_TRANSFER_PROHIBITED,
|
|
CLIENT_UPDATE_PROHIBITED
|
|
]
|
|
|
|
SERVER_STATUSES = [
|
|
SERVER_UPDATE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED
|
|
]
|
|
#
|
|
# END OF STATUSES
|
|
#
|
|
|
|
class << self
|
|
def search_by_query(query)
|
|
res = search(code_cont: query).result
|
|
res.reduce([]) { |o, v| o << { id: v[:id], display_key: "#{v.name} (#{v.code})" } }
|
|
end
|
|
|
|
def filter_by_states in_states
|
|
states = Array(in_states).dup
|
|
scope = all
|
|
|
|
# all contacts has state ok, so no need to filter by it
|
|
scope = scope.where("NOT contacts.statuses && ?::varchar[]", "{#{(STATUSES - [OK, LINKED]).join(',')}}") if states.delete(OK)
|
|
scope = scope.linked if states.delete(LINKED)
|
|
scope = scope.where("contacts.statuses @> ?::varchar[]", "{#{states.join(',')}}") if states.any?
|
|
scope
|
|
end
|
|
|
|
def admin_statuses
|
|
[
|
|
SERVER_UPDATE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED
|
|
]
|
|
end
|
|
|
|
def admin_statuses_map
|
|
[
|
|
['UpdateProhibited', SERVER_UPDATE_PROHIBITED],
|
|
['DeleteProhibited', SERVER_DELETE_PROHIBITED]
|
|
]
|
|
end
|
|
|
|
def to_csv
|
|
CSV.generate do |csv|
|
|
csv << column_names
|
|
all.each do |contact|
|
|
csv << contact.attributes.values_at(*column_names)
|
|
end
|
|
end
|
|
end
|
|
|
|
def pdf(html)
|
|
kit = PDFKit.new(html)
|
|
kit.to_pdf
|
|
end
|
|
|
|
def emails
|
|
pluck(:email)
|
|
end
|
|
|
|
def address_processing?
|
|
Setting.address_processing
|
|
end
|
|
|
|
def address_attribute_names
|
|
%w(
|
|
city
|
|
street
|
|
zip
|
|
country_code
|
|
state
|
|
)
|
|
end
|
|
|
|
def registrant_user_contacts(registrant_user, representable: true)
|
|
represented_contacts = registrant_user_direct_contacts(registrant_user)
|
|
.or(registrant_user_company_contacts(registrant_user))
|
|
|
|
return represented_contacts if representable
|
|
|
|
represented_contacts.or(registrant_user_indirect_contacts(registrant_user))
|
|
end
|
|
|
|
def registrant_user_direct_contacts(registrant_user)
|
|
where(ident_type: PRIV, ident: registrant_user.ident, ident_country_code: registrant_user
|
|
.country.alpha2)
|
|
end
|
|
|
|
def linked
|
|
sql = <<-SQL
|
|
EXISTS(SELECT 1 FROM domains WHERE domains.registrant_id = contacts.id) OR
|
|
EXISTS(SELECT 1 FROM domain_contacts WHERE domain_contacts.contact_id =
|
|
contacts.id)
|
|
SQL
|
|
|
|
where(sql)
|
|
end
|
|
|
|
def unlinked
|
|
where('NOT EXISTS(SELECT 1 FROM domains WHERE domains.registrant_id = contacts.id)
|
|
AND
|
|
NOT EXISTS(SELECT 1 FROM domain_contacts WHERE domain_contacts.contact_id =
|
|
contacts.id)')
|
|
end
|
|
|
|
def registrant_user_company_contacts(registrant_user)
|
|
ident = registrant_user.companies.collect(&:registration_number)
|
|
|
|
where(ident_type: ORG,
|
|
ident: ident,
|
|
ident_country_code: registrant_user.country.alpha2)
|
|
end
|
|
|
|
def registrant_user_indirect_contacts(registrant_user)
|
|
company_domains = Domain.registrant_user_indirect_domains(registrant_user)
|
|
company_contact_ids = company_domains.map(&:contacts).flatten.collect(&:id)
|
|
company_ids = Contact.registrant_user_company_contacts(registrant_user).collect(&:id)
|
|
total_ids = (company_contact_ids + company_ids).uniq
|
|
|
|
where(id: total_ids)
|
|
end
|
|
end
|
|
|
|
# kind of decorator in order to always return statuses
|
|
# if we use separate decorator, then we should add it
|
|
# to too many places
|
|
def statuses
|
|
calculated = Array(read_attribute(:statuses))
|
|
calculated.delete(Contact::OK)
|
|
calculated.delete(Contact::LINKED)
|
|
calculated << Contact::OK if calculated.empty?# && valid?
|
|
calculated << Contact::LINKED if linked?
|
|
|
|
calculated.uniq
|
|
end
|
|
|
|
def statuses= arr
|
|
write_attribute(:statuses, Array(arr).uniq)
|
|
end
|
|
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
def validate_html
|
|
self.class.columns.each do |column|
|
|
next unless column.type == :string
|
|
|
|
c_name = column.name
|
|
val = read_attribute(c_name)
|
|
if val && (val.include?('<') || val.include?('>') || val.include?('%3C') || val.include?('%3E'))
|
|
errors.add(c_name, :invalid)
|
|
return # want to run code faster
|
|
end
|
|
end
|
|
end
|
|
|
|
def org?
|
|
ident_type == ORG
|
|
end
|
|
|
|
# it might mean priv or birthday type
|
|
def priv?
|
|
!org?
|
|
end
|
|
|
|
def code=(code)
|
|
self[:code] = code if new_record? # cannot change code later
|
|
end
|
|
|
|
def generate_code
|
|
return nil unless new_record?
|
|
return nil if registrar.blank?
|
|
code = self[:code]
|
|
|
|
# custom code from client
|
|
# add prefix when needed
|
|
if code.present?
|
|
prefix, *custom_code = code.split(':')
|
|
code = custom_code.join(':') if prefix == registrar.code
|
|
end
|
|
|
|
code = SecureRandom.hex(4) if code.blank? || code == registrar.code
|
|
|
|
self[:code] = "#{registrar.code}:#{code}".upcase
|
|
end
|
|
|
|
alias_method :regenerate_code, :generate_code
|
|
|
|
def country
|
|
Country.new(country_code)
|
|
end
|
|
|
|
def to_upcase_country_code
|
|
self.ident_country_code = ident_country_code.upcase if ident_country_code
|
|
self.country_code = country_code.upcase if country_code
|
|
end
|
|
|
|
def validate_country_code
|
|
return unless country_code
|
|
errors.add(:country_code, :invalid) unless Country.new(country_code)
|
|
end
|
|
|
|
def related_domain_descriptions
|
|
@desc = {}
|
|
|
|
registrant_domains.each do |dom|
|
|
@desc[dom.name] ||= { id: dom.uuid, roles: [] }
|
|
@desc[dom.name][:roles] << :registrant
|
|
end
|
|
|
|
domain_contacts.each do |dc|
|
|
@desc[dc.domain.name] ||= { id: dc.domain.uuid, roles: [] }
|
|
@desc[dc.domain.name][:roles] << dc.name.downcase.to_sym
|
|
@desc[dc.domain.name] = @desc[dc.domain.name].compact
|
|
end
|
|
|
|
@desc
|
|
end
|
|
|
|
# Limits returned objects to 11
|
|
def related_domains
|
|
ids = DomainContact.select(:domain_id).where(contact_id: id).limit(11).map(&:domain_id).uniq
|
|
res = Domain.where(id: ids).or(Domain.where(registrant_id: id)).select(:name, :uuid).limit(11)
|
|
res.pluck(:name, :uuid).map { |name, id| { name: name, id: id } }
|
|
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 search_name
|
|
"#{code} #{name}"
|
|
end
|
|
|
|
def strip_email
|
|
self.email = email.to_s.strip
|
|
end
|
|
|
|
|
|
# what we can do load firstly by registrant
|
|
# if total is smaller than needed, the load more
|
|
# we also need to sort by valid_to
|
|
# todo: extract to drapper. Then we can remove Domain#roles
|
|
def all_domains(page: nil, per: nil, params:, requester: nil)
|
|
filter_sql = qualified_domain_ids(params[:domain_filter])
|
|
|
|
# get sorting rules
|
|
sorts = params.fetch(:sort, {}).first || []
|
|
sort = %w[name registrar_name valid_to].include?(sorts.first) ? sorts.first : 'valid_to'
|
|
order = %w[asc desc].include?(sorts.second) ? sorts.second : 'desc'
|
|
|
|
# fetch domains
|
|
domains = qualified_domain_name_list(requester, filter_sql)
|
|
domains = domains.includes(:registrar).page(page).per(per)
|
|
|
|
# using small rails hack to generate outer join
|
|
domains = if sorts.first == 'registrar_name'.freeze
|
|
domains.includes(:registrar).where.not(registrars: { id: nil })
|
|
.order("registrars.name #{order} NULLS LAST")
|
|
else
|
|
domains.order("#{sort} #{order} NULLS LAST")
|
|
end
|
|
|
|
# adding roles. Need here to make faster sqls
|
|
domain_c = Hash.new([])
|
|
registrant_domains.where(id: domains.map(&:id)).each do |d|
|
|
domain_c[d.id] |= ['Registrant'.freeze]
|
|
end
|
|
|
|
DomainContact.where(contact_id: id, domain_id: domains.map(&:id)).each do |d|
|
|
domain_c[d.domain_id] |= [d.type]
|
|
end
|
|
|
|
domains.each { |d| d.roles = domain_c[d.id].uniq }
|
|
|
|
domains
|
|
end
|
|
|
|
def qualified_domain_name_list(requester, filter_sql)
|
|
return Domain.where('domains.id IN (?)', filter_sql) if requester.blank?
|
|
|
|
registrant_user = RegistrantUser.find_or_initialize_by(registrant_ident:
|
|
"#{requester.ident_country_code}-#{requester.ident}")
|
|
begin
|
|
registrant_user.domains.where('domains.id IN (?)', filter_sql)
|
|
rescue CompanyRegister::NotAvailableError
|
|
registrant_user.direct_domains.where('domains.id IN (?)', filter_sql)
|
|
end
|
|
end
|
|
|
|
def qualified_domain_ids(domain_filter)
|
|
registrant_ids = registrant_domains.pluck(:id)
|
|
return registrant_ids if domain_filter == 'Registrant'
|
|
|
|
if %w[AdminDomainContact TechDomainContact].include? domain_filter
|
|
DomainContact.select('domain_id').where(contact_id: id, type: domain_filter)
|
|
else
|
|
(DomainContact.select('domain_id').where(contact_id: id).pluck(:domain_id) +
|
|
registrant_ids).uniq
|
|
end
|
|
end
|
|
|
|
def update_prohibited?
|
|
(statuses & [
|
|
CLIENT_UPDATE_PROHIBITED,
|
|
SERVER_UPDATE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED,
|
|
PENDING_CREATE,
|
|
PENDING_TRANSFER,
|
|
PENDING_UPDATE,
|
|
PENDING_DELETE
|
|
]).present?
|
|
end
|
|
|
|
def delete_prohibited?
|
|
(statuses & [
|
|
CLIENT_DELETE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED,
|
|
CLIENT_TRANSFER_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED,
|
|
PENDING_CREATE,
|
|
PENDING_TRANSFER,
|
|
PENDING_UPDATE,
|
|
PENDING_DELETE
|
|
]).present?
|
|
end
|
|
|
|
def clear_address_modifications
|
|
return unless modifies_address?
|
|
|
|
remove_address
|
|
end
|
|
|
|
def modifies_address?
|
|
modified = false
|
|
self.class.address_attribute_names.each { |field| modified = true if changes.key?(field) }
|
|
|
|
modified
|
|
end
|
|
|
|
def update_related_whois_records
|
|
# not doing anything if no real changes
|
|
ignored_columns = %w[updated_at created_at statuses status_notes]
|
|
return if saved_changes.slice(*(self.class.column_names - ignored_columns)).empty?
|
|
|
|
names = related_domain_descriptions.keys
|
|
UpdateWhoisRecordJob.perform_later(names, 'domain') if names.present?
|
|
end
|
|
|
|
def children_log
|
|
log = HashWithIndifferentAccess.new
|
|
log[:legal_documents]= [legal_document_id]
|
|
log
|
|
end
|
|
|
|
def remove_address
|
|
self.class.address_attribute_names.each do |attr_name|
|
|
self[attr_name.to_sym] = nil
|
|
end
|
|
end
|
|
|
|
def reg_no
|
|
return if priv?
|
|
ident
|
|
end
|
|
|
|
def id_code
|
|
return unless priv?
|
|
ident
|
|
end
|
|
|
|
def ident_country
|
|
Country.new(ident_country_code)
|
|
end
|
|
|
|
def linked?
|
|
registrant_domains.any? || domain_contacts.any?
|
|
end
|
|
|
|
def domain_names_with_roles
|
|
domain_names = {}
|
|
|
|
registrant_domains.pluck(:name).each do |domain_name|
|
|
domain_names[domain_name] ||= Set.new
|
|
domain_names[domain_name] << Registrant.name.underscore.to_sym
|
|
end
|
|
|
|
domain_contacts.each do |domain_contact|
|
|
domain_names[domain_contact.domain.name] ||= Set.new
|
|
domain_names[domain_contact.domain.name] << domain_contact.type.underscore.to_sym
|
|
end
|
|
|
|
domain_names
|
|
end
|
|
|
|
def address=(address)
|
|
self.street = address.street
|
|
self.zip = address.zip
|
|
self.city = address.city
|
|
self.state = address.state
|
|
self.country_code = address.country_code
|
|
end
|
|
|
|
def address
|
|
Address.new(street, zip, city, state, country_code)
|
|
end
|
|
|
|
def managed_by?(registrant_user)
|
|
ident == registrant_user.ident
|
|
end
|
|
|
|
def registrant?
|
|
registrant_domains.any?
|
|
end
|
|
|
|
def deletable?
|
|
!linked?
|
|
end
|
|
end
|