mirror of
https://github.com/internetee/registry.git
synced 2025-07-28 05:26:17 +02:00
- Add AgeValidation module for consistent age checks - Validate admin contacts must be at least 18 years old - Move age validation logic from Domain to shared module - Add tests for admin contact age validation - Fix JSON format for admin_contacts_allowed_ident_type setting This change ensures that administrative contacts must be adults (18+), using the same age validation logic as for registrants. The validation works with both birthday and Estonian ID formats. Settings are now properly stored as JSON strings for consistent parsing.
652 lines
19 KiB
Ruby
652 lines
19 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::Archivable
|
|
include Contact::CompanyRegister
|
|
include EmailVerifable
|
|
include AgeValidation
|
|
|
|
belongs_to :original, class_name: 'Contact'
|
|
belongs_to :registrar, required: true
|
|
has_many :domain_contacts
|
|
has_many :domains, through: :domain_contacts
|
|
has_many :legal_documents, as: :documentable
|
|
has_many :validation_events, as: :validation_eventable, dependent: :destroy
|
|
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')
|
|
}
|
|
|
|
scope :with_different_company_name, (lambda do |company|
|
|
where("ident = ? AND ident_country_code = 'EE' AND name != ?",
|
|
company.registration_number,
|
|
company.company_name)
|
|
end)
|
|
|
|
scope :with_different_registrant_name, (lambda do |user|
|
|
where('ident = ? AND ident_country_code = ? AND UPPER(name) != UPPER(?)',
|
|
user.ident,
|
|
user.country.alpha2,
|
|
user.username)
|
|
end)
|
|
|
|
NAME_REGEXP = /([\u00A1-\u00B3\u00B5-\u00BF\u0021-\u0026\u0028-\u002C\u003A-\u0040]|
|
|
[\u005B-\u005F\u007B-\u007E\u2040-\u206F\u20A0-\u20BF\u2100-\u218F])/x
|
|
|
|
validates :name, :email, presence: true
|
|
validates :name, length: { maximum: 255, message: :too_long_contact_name }
|
|
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? }
|
|
validates :registrant_publishable, inclusion: { in: [true, false] }, if: -> { registrant? }
|
|
# validates :registrant_publishable, inclusion: { in: [false] }, unless: -> { registrant? }
|
|
|
|
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? }
|
|
|
|
# TODO: remove after testing
|
|
after_save do
|
|
Rails.logger.info "Contact saved or updated: #{id}"
|
|
end
|
|
|
|
self.ignored_columns = %w[legacy_id legacy_history_id]
|
|
|
|
ORG = 'org'.freeze
|
|
PRIV = 'priv'.freeze
|
|
|
|
# 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'.freeze
|
|
|
|
#
|
|
# STATUSES
|
|
#
|
|
# Requests to delete the object MUST be rejected.
|
|
CLIENT_DELETE_PROHIBITED = 'clientDeleteProhibited'.freeze
|
|
SERVER_DELETE_PROHIBITED = 'serverDeleteProhibited'.freeze
|
|
|
|
# Requests to transfer the object MUST be rejected.
|
|
CLIENT_TRANSFER_PROHIBITED = 'clientTransferProhibited'.freeze
|
|
SERVER_TRANSFER_PROHIBITED = 'serverTransferProhibited'.freeze
|
|
|
|
# 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'.freeze
|
|
|
|
# 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'.freeze
|
|
|
|
# Requests to update the object (other than to remove this status) MUST be rejected.
|
|
CLIENT_UPDATE_PROHIBITED = 'clientUpdateProhibited'.freeze
|
|
SERVER_UPDATE_PROHIBITED = 'serverUpdateProhibited'.freeze
|
|
|
|
# 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'.freeze
|
|
# "pendingTransfer" status MUST NOT be combined with either
|
|
# "clientTransferProhibited" or "serverTransferProhibited" status.
|
|
PENDING_TRANSFER = 'pendingTransfer'.freeze
|
|
# "pendingUpdate" status MUST NOT be combined with either
|
|
# "clientUpdateProhibited" or "serverUpdateProhibited" status.
|
|
PENDING_UPDATE = 'pendingUpdate'.freeze
|
|
# "pendingDelete" MUST NOT be combined with either
|
|
# "clientDeleteProhibited" or "serverDeleteProhibited" status.
|
|
PENDING_DELETE = 'pendingDelete'.freeze
|
|
|
|
DISCLOSE_ATTRIBUTES = %w[
|
|
name
|
|
email
|
|
phone
|
|
registrant_publishable
|
|
address
|
|
fax
|
|
].freeze
|
|
|
|
OPEN_LEGAL_ATTRIBUTES = %w[
|
|
phone
|
|
].freeze
|
|
|
|
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
|
|
].freeze
|
|
|
|
CLIENT_STATUSES = [
|
|
CLIENT_DELETE_PROHIBITED, CLIENT_TRANSFER_PROHIBITED,
|
|
CLIENT_UPDATE_PROHIBITED
|
|
].freeze
|
|
|
|
SERVER_STATUSES = [
|
|
SERVER_UPDATE_PROHIBITED,
|
|
SERVER_DELETE_PROHIBITED,
|
|
SERVER_TRANSFER_PROHIBITED,
|
|
].freeze
|
|
#
|
|
# END OF STATUSES
|
|
#
|
|
|
|
class << self
|
|
def ransackable_associations(*)
|
|
authorizable_ransackable_associations
|
|
end
|
|
|
|
def ransackable_attributes(*)
|
|
authorizable_ransackable_attributes
|
|
end
|
|
|
|
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
|
|
if states.delete(OK)
|
|
scope = scope.where("NOT contacts.statuses && ?::varchar[]", "{#{(STATUSES - [OK, LINKED]).join(',')}}")
|
|
end
|
|
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 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.includes(:domain).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 name_disclosed_by_registrar(reg_id)
|
|
registrar_id == reg_id ? name : 'N/A'
|
|
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.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(filters)
|
|
rant_domains = registrant_domains.map { |d| { id: d.id, type: ['Registrant'] } }
|
|
contact_domains = domain_contacts.map { |dc| { id: dc.domain_id, type: [dc.type] } }
|
|
grouped_domains = group_by_id_and_type(rant_domains + contact_domains)
|
|
return grouped_domains.keys if filters.nil? || filters == ''
|
|
|
|
# use domain_filters.sort == v.sort if should be exact match
|
|
grouped_domains.reject { |_, v| ([].push(filters).flatten & v).empty? }.keys
|
|
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.where(contact_id: id, type: domain_filter).pluck(:domain_id)
|
|
# else
|
|
# (DomainContact.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
|
|
|
|
def ident_human_description
|
|
description = "[#{ident_country_code} #{ident_type}]"
|
|
description.prepend("#{ident} ") if ident.present?
|
|
|
|
description
|
|
end
|
|
|
|
def as_csv_row
|
|
[
|
|
name,
|
|
code,
|
|
ident_human_description,
|
|
email,
|
|
created_at.to_formatted_s(:db),
|
|
registrar,
|
|
phone,
|
|
]
|
|
end
|
|
|
|
def self.csv_header
|
|
['Name', 'ID', 'Ident', 'E-mail', 'Created at', 'Registrar', 'Phone']
|
|
end
|
|
|
|
private
|
|
|
|
def group_by_id_and_type(domains_hash_array)
|
|
domains_hash_array.group_by { |d| d[:id] }
|
|
.transform_values do |v|
|
|
v.each.with_object(:type)
|
|
.map(&:[]).flatten
|
|
end
|
|
end
|
|
end
|