diff --git a/Dockerfile b/Dockerfile index b3d32624d3..98d37d92b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ postgresql-client \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get remove -y google-chrome-stable +RUN apt-get purge -y google-chrome-stable +RUN apt-get autoremove -y && apt-get clean + +ENV CHROME_VERSION="128.0.6613.137" + +RUN wget -q "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" \ + && unzip chrome-linux64.zip -d /opt/ \ + && rm chrome-linux64.zip + +RUN wget -q "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" \ + && unzip chromedriver-linux64.zip -d /opt/ \ + && mv /opt/chromedriver-linux64/chromedriver /usr/local/bin/ \ + && rm -rf chromedriver-linux64.zip /opt/chromedriver-linux64 + RUN mkdir -p /opt/webapps/app/tmp/pids WORKDIR /opt/webapps/app COPY Gemfile Gemfile.lock ./ # ADD vendor/gems/omniauth-tara ./vendor/gems/omniauth-tara RUN gem install bundler && bundle install --jobs 20 --retry 5 +ENV PATH="/opt/chrome-linux64:${PATH}" + EXPOSE 3000 diff --git a/ai/sop/update_company_status_rake.md b/ai/sop/update_company_status_rake.md new file mode 100644 index 0000000000..821754b72f --- /dev/null +++ b/ai/sop/update_company_status_rake.md @@ -0,0 +1,17 @@ +# Steps to Update company_status.rake + +- [x] Modify the CSV output to include the contact type (role) information +- [x] Filter the output to include only Estonian organization type contacts +- [ ] Ensure only registrant contacts are included in the output +- [ ] Remove duplicate entries for the same organization +- [ ] Add a column to indicate if the contact is deleted due to an overdue annual statement +- [ ] Create a separate CSV file for invalid registrant contacts +- [ ] Update the existing CSV output to include only contacts that fail validation against the business registry and whitelist +- [ ] Add error handling and logging for better debugging +- [ ] Update the task description and comments to reflect the new functionality +- [ ] Add a new rake task or option to generate the separate registrant-only CSV file +- [ ] Implement validation against the business registry for Estonian organization contacts +- [ ] Implement validation against the whitelist for Estonian organization contacts +- [ ] Optimize the code for better performance, especially when dealing with large datasets +- [ ] Add unit tests for the new functionality +- [ ] Update the documentation to reflect the changes and new output format diff --git a/app/interactions/actions/contact_create.rb b/app/interactions/actions/contact_create.rb index c1b091a1fc..0793abcd2d 100644 --- a/app/interactions/actions/contact_create.rb +++ b/app/interactions/actions/contact_create.rb @@ -14,6 +14,7 @@ def call maybe_attach_legal_doc validate_ident maybe_change_email + maybe_company_is_relevant commit validate_contact end @@ -77,6 +78,16 @@ def validate_ident_birthday @error = true end + def maybe_company_is_relevant + return true unless contact.org? + + company_status = contact.return_company_status + return if [Contact::REGISTERED, Contact::LIQUIDATED].include? company_status + + contact.add_epp_error('2003', nil, 'ident', I18n.t('errors.messages.company_not_registered')) + @error = true + end + def maybe_attach_legal_doc ::Actions::BaseAction.attach_legal_doc_to_new(contact, legal_document, domain: false) end diff --git a/app/interactions/domains/force_delete/set_status.rb b/app/interactions/domains/force_delete/set_status.rb index b0a53ad822..770903d5a8 100644 --- a/app/interactions/domains/force_delete/set_status.rb +++ b/app/interactions/domains/force_delete/set_status.rb @@ -12,6 +12,8 @@ def force_delete_fast_track expire_warning_period_days + redemption_grace_period_days domain.force_delete_start = Time.zone.today + 1.day + + domain.status_notes[DomainStatus::FORCE_DELETE] = "Company no: #{domain.registrant.ident}" if reason == 'invalid_company' end def force_delete_soft diff --git a/app/jobs/company_register_status_job.rb b/app/jobs/company_register_status_job.rb new file mode 100644 index 0000000000..b8c3107b58 --- /dev/null +++ b/app/jobs/company_register_status_job.rb @@ -0,0 +1,113 @@ +require 'zip' + +class CompanyRegisterStatusJob < ApplicationJob + PAYMENT_STATEMENT_BUSINESS_REGISTRY_REASON = 'Kustutamiskanne dokumentide hoidjata' + + queue_as :default + + def perform(days_interval = 14, spam_time_delay = 1, batch_size = 100) + sampling_registrant_contact(days_interval).find_in_batches(batch_size: batch_size) do |contacts| + contacts.reject { |contact| whitelisted_company?(contact) }.each { |contact| proceed_company_status(contact, spam_time_delay) } + end + end + + private + + def proceed_company_status(contact, spam_time_delay) + # avoid spamming company register + sleep spam_time_delay + + company_status = contact.return_company_status + contact.update!(company_register_status: company_status, checked_company_at: Time.zone.now) + + puts "company id #{contact.id} status: #{company_status}" + + case company_status + when Contact::REGISTERED + lift_force_delete(contact) if check_for_force_delete(contact) + when Contact::LIQUIDATED + ContactInformMailer.company_liquidation(contact: contact).deliver_now + else + delete_process(contact) + end + + status = company_status.blank? ? Contact::DELETED : company_status + update_validation_company_status(contact:contact , status: status) + end + + def sampling_registrant_contact(days_interval) + Registrant.where(ident_type: 'org', ident_country_code: 'EE').where( + "(company_register_status IS NULL OR checked_company_at IS NULL) OR + (company_register_status = ? AND checked_company_at < ?) OR + company_register_status IN (?)", + Contact::REGISTERED, days_interval.days.ago, [Contact::LIQUIDATED, Contact::BANKRUPT, Contact::DELETED] + ) + + end + + def update_validation_company_status(contact:, status:) + contact.update(company_register_status: status, checked_company_at: Time.zone.now) + end + + def schedule_force_delete(contact) + contact.domains.each do |domain| + next if domain.schedule_force_delete? + + domain.schedule_force_delete( + type: :fast_track, + notify_by_email: true, + reason: 'invalid_company', + email: contact.email + ) + end + end + + def check_for_force_delete(contact) + contact.domains.any? && domain.status_notes[DomainStatus::FORCE_DELETE].include?("Company no: #{contact.ident}") do |domain| + domain.schedule_force_delete? + end + end + + def lift_force_delete(contact) + contact.domains.each(&:lift_force_delete) + end + + def delete_process(contact) + company_details_response = contact.return_company_details + + if company_details_response.empty? + schedule_force_delete(contact) + return + end + + kandeliik_tekstina = extract_kandeliik_tekstina(company_details_response) + + if kandeliik_tekstina == PAYMENT_STATEMENT_BUSINESS_REGISTRY_REASON + soft_delete_company(contact) + else + schedule_force_delete(contact) + end + end + + private + + def extract_kandeliik_tekstina(company_details_response) + company_details_response.first.kandeliik.last.last.kandeliik_tekstina + end + + def soft_delete_company(contact) + contact.domains.reject { |domain| domain.force_delete_scheduled? }.each do |domain| + domain.schedule_force_delete(type: :soft) + end + + puts "Soft delete process initiated for company: #{contact.name} with ID: #{contact.id}" + end + + def whitelisted_companies + @whitelisted_companies ||= ENV['whitelist_companies'].split(',') + end + + def whitelisted_company?(contact) + whitelisted_companies.include?(contact.ident) + end +end diff --git a/app/mailers/contact_inform_mailer.rb b/app/mailers/contact_inform_mailer.rb index bf5037cbfa..cb04037686 100644 --- a/app/mailers/contact_inform_mailer.rb +++ b/app/mailers/contact_inform_mailer.rb @@ -19,6 +19,13 @@ def notify_nameserver(contact:, domain:, nameserver:) mail(to: contact.email, subject: subject) end + def company_liquidation(contact:) + @registrant = contact + + subject = "Kas soovite oma .ee domeeni säilitada? / Do you wish to preserve your .ee registration?" + mail(to: contact.email, subject: subject) + end + private def address_processing diff --git a/app/models/concerns/domain/force_delete.rb b/app/models/concerns/domain/force_delete.rb index 3fa3bf627a..e0f98ea84e 100644 --- a/app/models/concerns/domain/force_delete.rb +++ b/app/models/concerns/domain/force_delete.rb @@ -32,7 +32,7 @@ def hold_status? def notification_template(explicit: nil) reason = explicit&.downcase - return reason if %w[invalid_email invalid_phone].include?(reason) + return reason if %w[invalid_email invalid_phone invalid_company].include?(reason) if contact_emails_verification_failed.present? 'invalid_email' diff --git a/app/models/contact.rb b/app/models/contact.rb index 0e076155bf..b7fc85dde8 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -8,6 +8,7 @@ class Contact < ApplicationRecord include Contact::Transferable include Contact::Identical include Contact::Archivable + include Contact::CompanyRegister include EmailVerifable belongs_to :original, class_name: 'Contact' diff --git a/app/models/contact/company_register.rb b/app/models/contact/company_register.rb new file mode 100644 index 0000000000..1c436eff75 --- /dev/null +++ b/app/models/contact/company_register.rb @@ -0,0 +1,38 @@ +module Contact::CompanyRegister + extend ActiveSupport::Concern + + REGISTERED = 'R'.freeze + LIQUIDATED = 'L'.freeze + BANKRUPT = 'N'.freeze + DELETED = 'K'.freeze + + def company_is_relevant? + company_register_status == REGISTERED && company_register_status == LIQUIDATED + end + + def return_company_status + return if return_company_data.blank? + + return_company_data.first[:status] + end + + def return_company_data + return unless org? + + company_register.simple_data(registration_number: ident) + rescue CompanyRegister::NotAvailableError + [] + end + + def return_company_details + return unless org? + + company_register.company_details(registration_number: ident) + rescue CompanyRegister::NotAvailableError + [] + end + + def company_register + @company_register ||= CompanyRegister::Client.new + end +end diff --git a/app/views/mailers/contact_inform_mailer/company_liquidation.html.erb b/app/views/mailers/contact_inform_mailer/company_liquidation.html.erb new file mode 100644 index 0000000000..f191889b65 --- /dev/null +++ b/app/views/mailers/contact_inform_mailer/company_liquidation.html.erb @@ -0,0 +1,34 @@ +
Lugupeetud ettevõtte <%= @registrant.name %> esindaja,
+ +Eesti Interneti Sihtasutusele (EIS) on äriregistri vahendusel teatavaks saanud, et ettevõtte <%= @registrant.name %> äriregistrikoodiga <%= @registrant.ident %> suhtes käib likvideerimismenetlus. Tuletame teile meelde, et tulenevalt .ee domeenireeglitest peab domeeni registreerijaks olema eksisteeriv era- või juriidiline isik. Seega käivitame ettevõtte likvideerimise järel sellele kuuluvate registreeringute kustutusmenetluse. Kustutusmenetluse tulemusena eemaldatakse .ee domeeninimi registrist ning vabaneb kõigile soovijatele taaskord registreerimiseks.
+Kui soovite ettevõttele kuuluvaid registreeritud domeene ka edaspidi kasutada, soovitame teil ette valmistada registreeringute üleandmine uuele omanikule enne praeguse omaniku likvideerimist. Lähemalt leiate selle kohta infot meie kodulehelt.
+ +.ee domeeni registris kuuluvad ettevõttele <%= @registrant.ident %> järgmised registreeringud:
+Lisaküsimuste korral võtke palun ühendust oma registripidajaga:
+<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrant.registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> + +Dear representative of <%= @registrant.name %>,
+ +The Estonian Internet Foundation (EIS) has found through the Estonian business register that the company <%= @registrant.name %> with the business registry code <%= @registrant.ident %> is in liquidation proceedings. Please note that according to the .ee domain regulation, the registrant of the domain must be an existing private or legal entity. Therefore, the registry will start the registration deletion procedure once the liquidation of the company has been finalized. After the registration deletion procedure, the domain name will be open to register again for everyone.
+If you want to continue to use the registered domains belonging to your company, we recommend you to prepare the transfer of the registrations to the new owner before the liquidation of the current owner will be finalized. Learn more on our website.
+ +The following registrations belong to the company <%= @registrant.ident %> in the .ee domain register:
+Should you have additional questions, please contact your registrar:
+<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrant.registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> +Lugupeetud domeeni <%= @domain.name %> registreerija/halduskontakt
+ +Eesti Interneti Sihtasutusele on saanud teatavaks, et juriidiline isik registrikoodiga <%= @domain.registrant.ident %> on äriregistrist kustutatud.
+ +Kuna äriregistrist kustutatud juriidiline isik ei saa olla domeeni registreerijaks, algas domeeni <%= @domain.name %> suhtes 45 päevane kustutusmenetlus. Menetluse käigus on domeen 15 esimest päeva internetis kättesaadav.
+ +Domeeni suhtes õigust omaval isikul on võimalus esitada domeeni <%= @domain.name %> registripidajale <%= @registrar.name %> domeeni üleandmise taotlus koos seda tõendava dokumendiga.
+ +Kui kontaktandmed ei ole <%= @delete_period_length %> päeva jooksul parandatud, läheb domeen <%= @domain.name %> <%= @domain.force_delete_date %> domeenioksjonile .ee oksjonikeskkonda. Juhul kui domeenile <%= @domain.name %> ei tehta oksjonil 24h möödudes pakkumist, domeen vabaneb ja on registreerimiseks vabalt kättesaadav kõigile huvilistele. Muude võimalike oksjoni tulemuste kohta loe siit.
+ +Lisaküsimuste korral võtke palun ühendust oma registripidajaga:
+<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> + +Dear registrant/administrative contact of .ee domain,
+ +Estonian Internet Foundation has learned that the legal person with registry code <%= @domain.registrant.ident %> has been deleted from the Business Registry.
+ +As a terminated legal person cannot be the registrant of a domain, a 45-day deletion process has started for the <%= @domain.name %> domain. For the first 15 days the domain will remain available on the Internet during the deletion process.
+ +The registrant holding a right to the domain name <%= @domain.name %> can submit a domain name transfer application to the registrar <%= @registrar.name %> with legal documentation.
+ +If the data is not fixed within <%= @delete_period_length %> days, the domain <%= @domain.name %> will go to domain auction on <%= @domain.force_delete_date %> in the .ee auction environment. If no offer is made for the domain <%= @domain.name %> at auction within 24 hours, the domain will be released and made freely available for registration to anyone interested on a first-come, first-served basis. Read more about other potential auction results here.
+ +Should you have additional questions, please contact your registrar:
+<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> +Уважаемый регистрант/административный контакт домена .ee
+ +Целевому учреждению Eesti Internet (EIS) стало известно, что юридическое лицо с регистрационным кодом <%= @domain.registrant.ident %> удалено из коммерческого реестра.
+ +Поскольку удаленное из коммерческого регистра юридическое лицо не может являться регистрантом домена, <%= Date.today.strftime('%d.%m.%y') %> начат 45-дневный процесс удаления домена <%= @domain.name %>. Домен доступен в интернете на протяжении 15 дней после начала процесса удаления.
+ +Лицо, обладающее правом на домен, может подать регистратору <%= @registrar.name %> домена <%= @domain.name %> ходатайство о передаче домена, представив вместе с ходатайством подтверждающие документы. Документы должны быть представлены регистратору в течение 45 дней.
+ +Если контактные данные не будут исправлены в течение <%= @delete_period_length %> дней, домен <%= @domain.name %> отправится <%= @domain.force_delete_date %> на доменный аукцион в аукционной среде.ee. Если в течение 24 часов в отношении домена <%= @domain.name %> е поступит предложений, домен освободится и станет доступным для всех желающих по принципу «кто раньше». О других возможных результатах аукциона читайте здесь.
+ +В случае возникновения дополнительных вопросов свяжитесь, пожалуйста, со своим регистратором: +<%= render 'mailers/shared/registrar/registrar.ru.html', registrar: @registrar %>
+ +<%= render 'mailers/shared/signatures/signature.ru.html' %> diff --git a/app/views/mailers/domain_delete_mailer/forced/invalid_company.text.erb b/app/views/mailers/domain_delete_mailer/forced/invalid_company.text.erb new file mode 100644 index 0000000000..f3aaca27a6 --- /dev/null +++ b/app/views/mailers/domain_delete_mailer/forced/invalid_company.text.erb @@ -0,0 +1,46 @@ +Lugupeetud domeeni <%= @domain.name %> registreerija/halduskontakt
+ +Eesti Interneti Sihtasutusele on saanud teatavaks, et juriidiline isik registrikoodiga <%= @domain.registrant.ident %> on äriregistrist kustutatud.
+ +Kuna äriregistrist kustutatud juriidiline isik ei saa olla domeeni registreerijaks, algas domeeni <%= @domain.name %> suhtes 45 päevane kustutusmenetlus. Menetluse käigus on domeen 15 esimest päeva internetis kättesaadav.
+ +Domeeni suhtes õigust omaval isikul on võimalus esitada domeeni <%= @domain.name %> registripidajale <%= @registrar.name %> domeeni üleandmise taotlus koos seda tõendava dokumendiga.
+ +Kui kontaktandmed ei ole <%= @delete_period_length %> päeva jooksul parandatud, läheb domeen <%= @domain.name %> <%= @domain.force_delete_date %> domeenioksjonile .ee oksjonikeskkonda. Juhul kui domeenile <%= @domain.name %> ei tehta oksjonil 24h möödudes pakkumist, domeen vabaneb ja on registreerimiseks vabalt kättesaadav kõigile huvilistele. Muude võimalike oksjoni tulemuste kohta loe siit.
+ +Lisaküsimuste korral võtke palun ühendust oma registripidajaga:
+<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> + +Dear registrant/administrative contact of .ee domain,
+ +Estonian Internet Foundation has learned that contact(s) phone number data of the domain <%= @domain.name %> are invalid.
+Estonian Internet Foundation has learned that the legal person with registry code <%= @domain.registrant.ident %> has been deleted from the Business Registry.
+ +As a terminated legal person cannot be the registrant of a domain, a 45-day deletion process has started for the <%= @domain.name %> domain. For the first 15 days the domain will remain available on the Internet during the deletion process.
+ +The registrant holding a right to the domain name <%= @domain.name %> can submit a domain name transfer application to the registrar <%= @registrar.name %> with legal documentation.
+ +If the data is not fixed within <%= @delete_period_length %> days, the domain <%= @domain.name %> will go to domain auction on <%= @domain.force_delete_date %> in the .ee auction environment. If no offer is made for the domain <%= @domain.name %> at auction within 24 hours, the domain will be released and made freely available for registration to anyone interested on a first-come, first-served basis. Read more about other potential auction results here.
+ +Should you have additional questions, please contact your registrar:
+<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> +Уважаемый регистрант/административный контакт домена .ee
+ +Целевому учреждению Eesti Internet (EIS) стало известно, что юридическое лицо с регистрационным кодом <%= @domain.registrant.ident %> удалено из коммерческого реестра.
+ +Поскольку удаленное из коммерческого регистра юридическое лицо не может являться регистрантом домена, <%= Date.today.strftime('%d.%m.%y') %> начат 45-дневный процесс удаления домена <%= @domain.name %>. Домен доступен в интернете на протяжении 15 дней после начала процесса удаления.
+ +Лицо, обладающее правом на домен, может подать регистратору <%= @registrar.name %> домена <%= @domain.name %> ходатайство о передаче домена, представив вместе с ходатайством подтверждающие документы. Документы должны быть представлены регистратору в течение 45 дней.
+ +Если контактные данные не будут исправлены в течение <%= @delete_period_length %> дней, домен <%= @domain.name %> отправится <%= @domain.force_delete_date %> на доменный аукцион в аукционной среде.ee. Если в течение 24 часов в отношении домена <%= @domain.name %> е поступит предложений, домен освободится и станет доступным для всех желающих по принципу «кто раньше». О других возможных результатах аукциона читайте здесь.
+ +В случае возникновения дополнительных вопросов свяжитесь, пожалуйста, со своим регистратором: + <%= render 'mailers/shared/registrar/registrar.ru.html', registrar: @registrar %>
+ +<%= render 'mailers/shared/signatures/signature.ru.html' %> diff --git a/config/application.yml.sample b/config/application.yml.sample index 0d2fd399cd..edc5872378 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -252,3 +252,7 @@ billing_system_integrated: 'true' secret_access_word: 'please-Give-Me-accesS' secret_word: 'this-secret-should-be-change' allow_accr_endspoints: 'true' + +whitelist_companies: + - '12345678' + - '87654321' \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index e9553aa207..d3d6ec6707 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -202,6 +202,7 @@ en: invalid_cert: 'Invalid certificate' failed_epp_conn: 'Failed to open connection to EPP server!' epp_conn_error: 'CONNECTION ERROR - Is the EPP server running?' + company_not_registered: 'Company is not registered' code: 'Code' action: 'Action' diff --git a/db/migrate/20230710120154_add_checked_company_at_to_contacts.rb b/db/migrate/20230710120154_add_checked_company_at_to_contacts.rb new file mode 100644 index 0000000000..e7f9d5f1a7 --- /dev/null +++ b/db/migrate/20230710120154_add_checked_company_at_to_contacts.rb @@ -0,0 +1,5 @@ +class AddCheckedCompanyAtToContacts < ActiveRecord::Migration[6.1] + def change + add_column :contacts, :checked_company_at, :datetime + end +end diff --git a/db/migrate/20230711083811_add_company_register_status_to_contacts.rb b/db/migrate/20230711083811_add_company_register_status_to_contacts.rb new file mode 100644 index 0000000000..615151721e --- /dev/null +++ b/db/migrate/20230711083811_add_company_register_status_to_contacts.rb @@ -0,0 +1,5 @@ +class AddCompanyRegisterStatusToContacts < ActiveRecord::Migration[6.1] + def change + add_column :contacts, :company_register_status, :string + end +end diff --git a/db/structure.sql b/db/structure.sql index fc2c2df125..6b189b54c1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -692,7 +692,9 @@ CREATE TABLE public.contacts ( uuid uuid DEFAULT public.gen_random_uuid() NOT NULL, disclosed_attributes character varying[] DEFAULT '{}'::character varying[] NOT NULL, email_history character varying, - registrant_publishable boolean DEFAULT false + registrant_publishable boolean DEFAULT false, + checked_company_at timestamp without time zone, + company_register_status character varying ); @@ -1099,6 +1101,45 @@ CREATE SEQUENCE public.email_addresses_verifications_id_seq ALTER SEQUENCE public.email_addresses_verifications_id_seq OWNED BY public.email_addresses_verifications.id; +-- +-- Name: epp_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.epp_logs ( + id bigint NOT NULL, + request text, + response text, + request_command character varying(255), + request_object character varying, + request_successful boolean, + api_user_name character varying(255), + api_user_registrar character varying(255), + ip character varying(255), + created_at timestamp without time zone, + updated_at timestamp without time zone, + uuid character varying +); + + +-- +-- Name: epp_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.epp_logs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: epp_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.epp_logs_id_seq OWNED BY public.epp_logs.id; + + -- -- Name: epp_sessions; Type: TABLE; Schema: public; Owner: - -- @@ -2553,6 +2594,45 @@ CREATE SEQUENCE public.registrars_id_seq ALTER SEQUENCE public.registrars_id_seq OWNED BY public.registrars.id; +-- +-- Name: repp_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.repp_logs ( + id bigint NOT NULL, + request_path character varying(255), + request_method character varying(255), + request_params text, + response text, + response_code character varying(255), + api_user_name character varying(255), + api_user_registrar character varying(255), + ip character varying(255), + created_at timestamp without time zone, + updated_at timestamp without time zone, + uuid character varying +); + + +-- +-- Name: repp_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.repp_logs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: repp_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.repp_logs_id_seq OWNED BY public.repp_logs.id; + + -- -- Name: reserved_domains; Type: TABLE; Schema: public; Owner: - -- @@ -3061,6 +3141,13 @@ ALTER TABLE ONLY public.email_addresses_validations ALTER COLUMN id SET DEFAULT ALTER TABLE ONLY public.email_addresses_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_addresses_verifications_id_seq'::regclass); +-- +-- Name: epp_logs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.epp_logs ALTER COLUMN id SET DEFAULT nextval('public.epp_logs_id_seq'::regclass); + + -- -- Name: epp_sessions id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3320,6 +3407,13 @@ ALTER TABLE ONLY public.registrant_verifications ALTER COLUMN id SET DEFAULT nex ALTER TABLE ONLY public.registrars ALTER COLUMN id SET DEFAULT nextval('public.registrars_id_seq'::regclass); +-- +-- Name: repp_logs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.repp_logs ALTER COLUMN id SET DEFAULT nextval('public.repp_logs_id_seq'::regclass); + + -- -- Name: reserved_domains id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3567,6 +3661,14 @@ ALTER TABLE ONLY public.email_addresses_verifications ADD CONSTRAINT email_addresses_verifications_pkey PRIMARY KEY (id); +-- +-- Name: epp_logs epp_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.epp_logs + ADD CONSTRAINT epp_logs_pkey PRIMARY KEY (id); + + -- -- Name: epp_sessions epp_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3863,6 +3965,14 @@ ALTER TABLE ONLY public.registrars ADD CONSTRAINT registrars_pkey PRIMARY KEY (id); +-- +-- Name: repp_logs repp_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.repp_logs + ADD CONSTRAINT repp_logs_pkey PRIMARY KEY (id); + + -- -- Name: reserved_domains reserved_domains_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4055,6 +4165,13 @@ ALTER TABLE ONLY public.zones ADD CONSTRAINT zones_pkey PRIMARY KEY (id); +-- +-- Name: epp_logs_uuid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX epp_logs_uuid ON public.epp_logs USING btree (uuid); + + -- -- Name: index_account_activities_on_account_id; Type: INDEX; Schema: public; Owner: - -- @@ -4755,6 +4872,13 @@ CREATE INDEX log_domains_object_legacy_id ON public.log_contacts USING btree ((( CREATE INDEX log_nameservers_object_legacy_id ON public.log_contacts USING btree ((((object ->> 'legacy_domain_id'::text))::integer)); +-- +-- Name: repp_logs_uuid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX repp_logs_uuid ON public.repp_logs USING btree (uuid); + + -- -- Name: unique_data_migrations; Type: INDEX; Schema: public; Owner: - -- @@ -4992,6 +5116,7 @@ ALTER TABLE ONLY public.users SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('0'), ('20140616073945'), ('20140620130107'), ('20140627082711'), @@ -5470,7 +5595,12 @@ INSERT INTO "schema_migrations" (version) VALUES ('20221214073933'), ('20221214074252'), ('20230531111154'), +('20230612094319'), +('20230612094326'), +('20230612094335'), ('20230707084741'), +('20230710120154'), +('20230711083811'), ('20240816091049'), ('20240816092636'); diff --git a/ettevotja_rekvisiidid__lihtandmed.csv.zip b/ettevotja_rekvisiidid__lihtandmed.csv.zip new file mode 100644 index 0000000000..0e28fcb109 Binary files /dev/null and b/ettevotja_rekvisiidid__lihtandmed.csv.zip differ diff --git a/lib/tasks/company_status.rake b/lib/tasks/company_status.rake new file mode 100644 index 0000000000..87d6fadf6d --- /dev/null +++ b/lib/tasks/company_status.rake @@ -0,0 +1,209 @@ +require 'csv' +require 'open-uri' +require 'zip' +require 'net/http' +require 'uri' +require 'optparse' +require 'rake_option_parser_boilerplate' + +namespace :company_status do + # bundle exec rake company_status:check_all -- --open_data_file_path=tmp/ettevotja_rekvisiidid__lihtandmed.csv --missing_companies_output_path=tmp/missing_companies_in_business_registry.csv --deleted_companies_output_path=tmp/deleted_companies_from_business_registry.csv --download_path=https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip --soft_delete_enable=false --registrants_only=true + desc 'Get Estonian companies status from Business Registry.' + + DELETED_FROM_REGISTRY_STATUS = 'K' + DESTINATION = Rails.root.join('tmp').to_s + '/' + COMPANY_STATUS = 'ettevotja_staatus' + BUSINESS_REGISTRY_CODE = 'ariregistri_kood' + + task :check_all => :environment do + options = initialize_rake_task + + open_data_file_path = options[:open_data_file_path] + missing_companies_in_business_registry_path = options[:missing_companies_output_path] + deleted_companies_from_business_registry_path = options[:deleted_companies_output_path] + download_path = options[:download_path] + soft_delete_enable = options[:soft_delete_enable] + downloaded_filename = File.basename(URI(download_path).path) + are_registrants_only = options[:registrants_only] + + puts "*** Run 1 step. Downloading fresh open data file. ***" + remove_old_file(DESTINATION + downloaded_filename) + download_open_data_file(download_path, downloaded_filename) + unzip_file(downloaded_filename, DESTINATION) + + puts "*** Run 2 step. I am collecting data from open business registry sources. ***" + company_data = collect_company_data(open_data_file_path) + + puts "*** Run 3 step. I process companies, update their information, and sort them into different files based on whether the companies are missing or removed from the business registry ***" + + whitelisted_companies = JSON.parse(ENV['whitelist_companies']) # ["12345678", "87654321"] + + contacts_query = Contact.where(ident_type: 'org', ident_country_code: 'EE') + + if are_registrants_only + contacts_query = contacts_query.joins(:registrant_domains).distinct + end + + unique_contacts = contacts_query.to_a.uniq(&:ident) + + unique_contacts.each do |contact| + next if whitelisted_companies.include?(contact.ident) + + if company_data.key?(contact.ident) + update_company_status(contact: contact, status: company_data[contact.ident][COMPANY_STATUS]) + puts "Company: #{contact.name} with ident: #{contact.ident} and ID: #{contact.id} has status: #{company_data[contact.ident][COMPANY_STATUS]}" + else + update_company_status(contact: contact, status: 'K') + sort_companies_to_files( + contact: contact, + missing_companies_in_business_registry_path: missing_companies_in_business_registry_path, + deleted_companies_from_business_registry_path: deleted_companies_from_business_registry_path, + soft_delete_enable: soft_delete_enable + ) + end + end + + puts '*** Done ***' + end + + private + + def initialize_rake_task + open_data_file_path = "#{DESTINATION}ettevotja_rekvisiidid__lihtandmed.csv" + missing_companies_in_business_registry_path = "#{DESTINATION}missing_companies_in_business_registry.csv" + deleted_companies_from_business_registry_path = "#{DESTINATION}deleted_companies_from_business_registry.csv" + url = 'https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip' + + options = { + open_data_file_path: open_data_file_path, + missing_companies_output_path: missing_companies_in_business_registry_path, + deleted_companies_output_path: deleted_companies_from_business_registry_path, + download_path: url, + soft_delete_enable: false, + registrants_only: false, + } + + banner = 'Usage: rake companies:check_all -- [options]' + RakeOptionParserBoilerplate.process_args(options: options, + banner: banner, + hash: companies_opts_hash) + end + + def companies_opts_hash + { + open_data_file_path: ['-o [OPEN_DATA_FILE_PATH]', '--open_data_file_path [DOMAIN_NAME]', String], + missing_companies_output_path: ['-m [MISSING_COMPANIES_OUTPUT_PATH]', '--missing_companies_output_path [MISSING_COMPANIES_OUTPUT_PATH]', String], + deleted_companies_output_path: ['-s [DELETED_COMPANIES_OUTPUT_PATH]', '--deleted_companies_output_path [DELETED_COMPANIES_OUTPUT_PATH]', String], + download_path: ['-d [DOWNLOAD_PATH]', '--download_path [DOWNLOAD_PATH]', String], + soft_delete_enable: ['-e [SOFT_DELETE_ENABLE]', '--soft_delete_enable [SOFT_DELETE_ENABLE]', FalseClass], + registrants_only: ['-r', '--registrants_only [REGISTRANTS_ONLY]', FalseClass], + } + end + + def remove_old_file(output_file_path) + FileUtils.rm(output_file_path) if File.exist?(output_file_path) + end + + + def unzip_file(filename, destination) + Zip::File.open(filename) do |zip_file| + zip_file.each do |entry| + entry.extract(File.join(destination, entry.name)) { true } + end + end + + puts "Archive invoke to #{destination}" + end + + def collect_company_data(open_data_file_path) + company_data = {} + + CSV.foreach(open_data_file_path, headers: true, col_sep: ';', quote_char: '"', liberal_parsing: true) do |row| + company_data[row[BUSINESS_REGISTRY_CODE]] = row + end + + company_data + end + + def download_open_data_file(url, filename) + uri = URI(url) + + Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| + request = Net::HTTP::Get.new(uri) + response = http.request(request) + + if response.code == '200' + File.open(filename, 'wb') do |file| + file.write(response.body) + end + else + puts "Failed to download file: #{response.code} #{response.message}" + end + end + + puts "File saved as #{filename}" + end + + def update_company_status(contact:, status:) + contact.update(company_register_status: status, checked_company_at: Time.zone.now) + end + + def put_company_to_missing_file(contact:, path:) + write_to_csv_file(csv_file_path: path, headers: ["ID", "Ident", "Name", "Contact Type"], attrs: [contact.id, contact.ident, contact.name, determine_contact_type(contact)]) + end + + def sort_companies_to_files(contact:, missing_companies_in_business_registry_path:, deleted_companies_from_business_registry_path:, soft_delete_enable:) + sleep 1 + resp = contact.return_company_details + + if resp.empty? + put_company_to_missing_file(contact: contact, path: missing_companies_in_business_registry_path) + puts "Company: #{contact.name} with ident: #{contact.ident} and ID: #{contact.id} is missing in registry, company id: #{contact.id}" + soft_delete_company(contact) if soft_delete_enable + else + status = resp.first.status.upcase + kandeliik_type = resp.first.kandeliik.last.last.kandeliik + kandeliik_tekstina = resp.first.kandeliik.last.last.kandeliik_tekstina + kande_kpv = resp.first.kandeliik.last.last.kande_kpv + + if status == DELETED_FROM_REGISTRY_STATUS + csv_file_path = deleted_companies_from_business_registry_path + headers = ["ID", "Ident", "Name", "Status", "Kandeliik Type", "Kandeliik Tekstina", "kande_kpv", "Contact Type"] + attrs = [contact.id, contact.ident, contact.name, status, kandeliik_type, kandeliik_tekstina, kande_kpv, determine_contact_type(contact)] + write_to_csv_file(csv_file_path: csv_file_path, headers: headers, attrs: attrs) + + puts "Company: #{contact.name} with ident: #{contact.ident} and ID: #{contact.id} has status #{status}, company id: #{contact.id}" + soft_delete_company(contact) if soft_delete_enable + end + end + end + + def determine_contact_type(contact) + roles = [] + roles << 'Registrant' if contact.registrant_domains.any? + roles += contact.domain_contacts.pluck(:type).uniq if contact.domain_contacts.any? + roles << 'Unknown' if roles.empty? + roles.join(', ') + end + + def soft_delete_company(contact) + contact.domains.reject { |domain| domain.force_delete_scheduled? }.each do |domain| + domain.schedule_force_delete(type: :soft) + end + + puts "Soft delete process initiated for company: #{contact.name} with ID: #{contact.id}" + end + + def write_to_csv_file(csv_file_path:, headers:, attrs:) + write_headers = !File.exist?(csv_file_path) + + begin + CSV.open(csv_file_path, "ab", write_headers: write_headers, headers: headers) do |csv| + csv << attrs + end + puts "Successfully wrote to CSV: #{csv_file_path}" + rescue => e + puts "Error writing to CSV: #{e.message}" + end + end +end diff --git a/test/tasks/company_status_task_test.rb b/test/tasks/company_status_task_test.rb new file mode 100644 index 0000000000..3f40df88c5 --- /dev/null +++ b/test/tasks/company_status_task_test.rb @@ -0,0 +1,239 @@ +require 'test_helper' +require 'webmock/minitest' +require 'tempfile' +require 'csv' +require 'zip' + +module CompanyStatusTaskTestOverrides + def download_open_data_file(url, filename) + uri = URI(url) + + Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| + request = Net::HTTP::Get.new(uri) + response = http.request(request) + + if response.code == '200' + File.open(filename, 'wb') do |file| + file.write(response.body) + end + else + puts "Failed to download file: #{response.code} #{response.message}" + end + end + + puts "File saved as #{filename}" + end + + def unzip_file(filename, destination) + Zip::File.open(filename) do |zip_file| + zip_file.each do |entry| + entry_path = File.join(destination, entry.name) + entry.extract(entry_path) { true } # Overwrite existing files + end + end + true + end + + def collect_company_data(open_data_file_path) + $test_options = open_data_file_path + # Return test data + { '12345678' => { 'ettevotja_staatus' => 'active' } } + end + + def update_company_status(contact:, status:) + # Do nothing + end + + def sort_companies_to_files(contact:, missing_companies_in_business_registry_path:, deleted_companies_from_business_registry_path:, soft_delete_enable:) + # Do nothing + end + + def initialize_rake_task + options = { + open_data_file_path: "#{DESTINATION}ettevotja_rekvisiidid__lihtandmed.csv", + missing_companies_output_path: "#{DESTINATION}missing_companies_in_business_registry.csv", + deleted_companies_output_path: "#{DESTINATION}deleted_companies_from_business_registry.csv", + download_path: 'https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip', + soft_delete_enable: false, + registrants_only: false, + } + + # Process command line arguments + RakeOptionParserBoilerplate.process_args( + options: options, + banner: 'Usage: rake company_status:check_all [options]', + hash: { + open_data_file_path: ['-o', '--open_data_file_path PATH', String], + missing_companies_output_path: ['-m', '--missing_companies_output_path PATH', String], + deleted_companies_output_path: ['-d', '--deleted_companies_output_path PATH', String], + download_path: ['-u', '--download_path URL', String], + soft_delete_enable: ['-s', '--soft_delete_enable', :NONE], + registrants_only: ['-r', '--registrants_only', :NONE] + } + ) + + options + end +end + +class CompanyStatusTaskTest < ActiveSupport::TestCase + include CompanyStatusTaskTestOverrides + + def setup + super # Always call super when overriding setup + + # Create temporary CSV file with test data + @temp_csv = Tempfile.new(['test_data', '.csv']) + CSV.open(@temp_csv.path, 'wb') do |csv| + csv << ['ariregistri_kood', 'ettevotja_staatus'] + csv << ['12345678', 'active'] + end + + @temp_csv_path = @temp_csv.path + $temp_csv_path = @temp_csv.path # Set the global variable + + # Create temporary zip file containing our CSV + @temp_zip = Tempfile.new(['test_data', '.zip']) + Zip::File.open(@temp_zip.path, Zip::File::CREATE) do |zipfile| + zipfile.add('ettevotja_rekvisiidid__lihtandmed.csv', @temp_csv_path) + end + + # Stub HTTP request + stub_request(:get, 'https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip') + .to_return(status: 200, body: File.read(@temp_zip.path), headers: {}) + + # Prepend the module to the main object to override methods + main = TOPLEVEL_BINDING.eval('self') + main.singleton_class.prepend(CompanyStatusTaskTestOverrides) + end + + def teardown + super # Always call super when overriding teardown + + @temp_csv.close if @temp_csv + @temp_csv.unlink if @temp_csv + @temp_zip.close if @temp_zip + @temp_zip.unlink if @temp_zip + WebMock.reset! + end + + test "initialize_rake_task sets default options correctly and handles file processing" do + stub_request(:get, 'https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip') + .to_return(status: 200, body: File.read(@temp_zip.path), headers: {}) + + ENV['whitelist_companies'] = '["12345678", "87654321"]' + $test_options = nil + + # No need to prepend again; it's already done in setup + + # Stub external dependencies + RakeOptionParserBoilerplate.stub :process_args, ->(options:, banner:, hash:) { options } do + run_task + + # Assertions + assert_not_nil $test_options, "Options should not be nil" + + expected_path = Rails.root.join('tmp', 'ettevotja_rekvisiidid__lihtandmed.csv').to_s + assert_equal expected_path, $test_options + + # Add more assertions as needed + end + + assert_requested :get, 'https://avaandmed.ariregister.rik.ee/sites/default/files/avaandmed/ettevotja_rekvisiidid__lihtandmed.csv.zip' + end + + test "initialize_rake_task processes command line arguments" do + simulated_args = [ + '--open_data_file_path=/custom/path.csv', + '--missing_companies_output_path=/custom/missing.csv', + '--deleted_companies_output_path=/custom/deleted.csv', + '--download_path=https://example.com/custom.zip', + '--soft_delete_enable', + '--registrants_only' + ] + + # Replace ARGV with simulated arguments + original_argv = ARGV.dup + ARGV.replace(simulated_args) + + # Stub RakeOptionParserBoilerplate to process ARGV + RakeOptionParserBoilerplate.stub :process_args, ->(options:, banner:, hash:) { + OptionParser.new do |opts| + hash.each do |key, (short, long, type)| + opts.on(*[short, long, type].compact) do |value| + # Convert string 'true'/'false' to boolean if needed + if [TrueClass, FalseClass].include?(type) + value = true + end + options[key] = value + end + end + end.parse!(ARGV) + options + } do + options = initialize_rake_task + + # Assertions + assert_equal '/custom/path.csv', options[:open_data_file_path] + assert_equal '/custom/missing.csv', options[:missing_companies_output_path] + assert_equal '/custom/deleted.csv', options[:deleted_companies_output_path] + assert_equal 'https://example.com/custom.zip', options[:download_path] + assert_equal true, options[:soft_delete_enable] + assert_equal true, options[:registrants_only] + end + + # Restore ARGV + ARGV.replace(original_argv) + end + + test "download_open_data_file downloads file successfully" do + # Setup a temporary filename + temp_filename = 'test_download.zip' + + # Stub the HTTP request + stub_request(:get, 'https://example.com/test.zip') + .to_return(status: 200, body: 'Test content', headers: {}) + + # Call the actual method + download_open_data_file('https://example.com/test.zip', temp_filename) + + # Assertions + assert File.exist?(temp_filename), "File should exist after download" + assert_equal 'Test content', File.read(temp_filename) + + assert_requested :get, 'https://example.com/test.zip' + + # Cleanup + File.delete(temp_filename) if File.exist?(temp_filename) + end + + test "unzip_file extracts contents correctly" do + # Create a temporary zip file with known content + temp_zip = Tempfile.new(['test', '.zip']) + temp_dir = Dir.mktmpdir + + Zip::File.open(temp_zip.path, Zip::File::CREATE) do |zipfile| + zipfile.get_output_stream('test.txt') { |f| f.write 'Hello, world!' } + end + + # Call the method + unzip_file(temp_zip.path, temp_dir) + + # Assertions + extracted_file = File.join(temp_dir, 'test.txt') + puts "Extracted file path: #{extracted_file}" # Add debug information + puts "Directory contents: #{Dir.entries(temp_dir)}" # Add debug information + + assert File.exist?(extracted_file), "File should be extracted" + assert_equal 'Hello, world!', File.read(extracted_file) + + # Cleanup + temp_zip.close + temp_zip.unlink + FileUtils.remove_entry(temp_dir) + end + + def run_task + Rake::Task['company_status:check_all'].execute + end +end diff --git a/test_data 2.csv b/test_data 2.csv new file mode 100644 index 0000000000..25728e63df --- /dev/null +++ b/test_data 2.csv @@ -0,0 +1,2 @@ +company_code,company_name,status +12345678,Test Company,active diff --git a/test_data.csv b/test_data.csv new file mode 100644 index 0000000000..25728e63df --- /dev/null +++ b/test_data.csv @@ -0,0 +1,2 @@ +company_code,company_name,status +12345678,Test Company,active