diff --git a/.gitignore b/.gitignore index cb4bf2c3b5..d72d03e6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ CLAUDE.md AGENT.md own AGENT.md - +/docs/ +.repowise diff --git a/CHANGELOG.md b/CHANGELOG.md index b88ba60f11..a58f32aee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +10.08.2026 +* Registrars can now cancel a pending registrant change https://github.com/internetee/registry/issues/2939 + 23.07.2026 * Operations with pending status now return result code 1001 in REPP https://github.com/internetee/registry/issues/2940 * Fixed case sensitivity issue for REPP requests https://github.com/internetee/registry/issues/2943 diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index 094fe595a7..e588e844c7 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -9,6 +9,12 @@ class DomainsController < BaseController THROTTLED_ACTIONS = %i[info create check renew update transfer delete].freeze include Shunter::Integration::Throttle + # Everything Deserializers::Xml::DomainUpdate can emit besides the registrant itself. + # :domain and :registrar_id are always present, :legal_document may be mandatory for + # the registrar even when cancelling. + UPDATE_KEYS_BESIDES_REGISTRANT = %i[contacts nameservers dns_keys statuses transfer_code + reserved_pw].freeze + def info authorize! :info, @domain @@ -47,6 +53,9 @@ def update registrar_id = current_user.registrar.id update_params = ::Deserializers::Xml::DomainUpdate.new(params[:parsed_frame], registrar_id).call + + return cancel_pending_update if cancels_pending_update?(update_params) + action = Actions::DomainUpdate.new(@domain, update_params, false) unless action.call handle_errors(@domain) @@ -134,6 +143,33 @@ def transfer private + # EPP has no command to manipulate pending operations (RFC 3731 covers transfer only), + # so a domain:update requesting the registrant the domain already has is treated as a + # request to cancel the pending registrant change. Same idea as domain:renew cancelling + # pendingDelete in Epp::Domain#renew. + def cancels_pending_update?(update_params) + return false unless @domain.pending_update? + return false if update_params[:registrant].blank? + + requested = Registrant.find_by(code: update_params[:registrant][:code]) + return false unless requested&.id == @domain.registrant_id + + (update_params.keys & UPDATE_KEYS_BESIDES_REGISTRANT).empty? + end + + def cancel_pending_update + result = ::Domains::CancelPendingUpdate.run(domain: @domain, + initiator: current_user.username) + unless result.valid? + @domain.add_epp_error('2304', 'status', DomainStatus::PENDING_UPDATE, + result.errors.full_messages.join(', ')) + handle_errors(@domain) + return + end + + render_epp_response('/epp/domains/success') + end + def validate_info @prefix = 'info > info >' requires('name') diff --git a/app/controllers/repp/v1/domains/pending_updates_controller.rb b/app/controllers/repp/v1/domains/pending_updates_controller.rb new file mode 100644 index 0000000000..0e36ec1847 --- /dev/null +++ b/app/controllers/repp/v1/domains/pending_updates_controller.rb @@ -0,0 +1,29 @@ +module Repp + module V1 + module Domains + class PendingUpdatesController < BaseController + before_action :set_domain + + THROTTLED_ACTIONS = %i[destroy].freeze + include Shunter::Integration::Throttle + + api :DELETE, '/repp/v1/domains/:domain_name/pending_update' + param :domain_name, String, desc: 'Domain name' + desc 'Cancel a pending registrant change of a specific domain' + def destroy + authorize!(:update, @domain) + + result = ::Domains::CancelPendingUpdate.run(domain: @domain, + initiator: current_user.username) + unless result.valid? + @domain.add_epp_error('2304', 'status', DomainStatus::PENDING_UPDATE, + result.errors.full_messages.join(', ')) + return handle_errors(@domain) + end + + render_success(data: { domain: { name: @domain.name } }) + end + end + end + end +end diff --git a/app/interactions/domains/cancel_pending_update.rb b/app/interactions/domains/cancel_pending_update.rb new file mode 100644 index 0000000000..1a740ec2b0 --- /dev/null +++ b/app/interactions/domains/cancel_pending_update.rb @@ -0,0 +1,52 @@ +module Domains + class CancelPendingUpdate < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain with a pending registrant change' + string :initiator, + default: nil + + validate :domain_has_pending_update + + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - cancelled by"\ + " #{initiator}" + + ActiveRecord::Base.transaction do + notify_registrants + clean_pendings! + end + + UpdateWhoisRecordJob.perform_later(domain.name, 'domain') + end + + private + + def domain_has_pending_update + return if domain&.pending_update? + + errors.add(:domain, I18n.t(:object_status_prohibits_operation)) + end + + # Both parties already got a confirmation link that is about to become invalid, + # so they are notified before the verification data is wiped. + def notify_registrants + RegistrantChangeMailer.cancelled(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant, + send_to: [domain.new_registrant_email, + domain.registrant.email]).deliver_later + end + + def clean_pendings! + domain.is_admin = true + # Has to happen before save, otherwise before_update reinstates pendingUpdate + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + domain.pending_json = {} + domain.statuses.delete(DomainStatus::PENDING_UPDATE) + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.save! + end + end +end diff --git a/app/interactions/domains/update_confirm/process_action.rb b/app/interactions/domains/update_confirm/process_action.rb index 07c3a6dd31..03d2463476 100644 --- a/app/interactions/domains/update_confirm/process_action.rb +++ b/app/interactions/domains/update_confirm/process_action.rb @@ -2,6 +2,10 @@ module Domains module UpdateConfirm class ProcessAction < Base def execute + # The registrant decision arrives asynchronously, so the pending update may already + # be gone by now - cancelled by the registrar or cleaned up by the expiry cron. + return unless domain.pending_update? + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - #{action} by"\ " #{initiator}" diff --git a/app/mailers/registrant_change_mailer.rb b/app/mailers/registrant_change_mailer.rb index 101e6c3e17..dd0a8cb980 100644 --- a/app/mailers/registrant_change_mailer.rb +++ b/app/mailers/registrant_change_mailer.rb @@ -38,6 +38,15 @@ def rejected(domain:, registrar:, registrant:) mail(to: domain.new_registrant_email, subject: subject) end + def cancelled(domain:, registrar:, registrant:, send_to:) + @domain = DomainPresenter.new(domain: domain, view: view_context) + @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) + @registrant = RegistrantPresenter.new(registrant: registrant, view: view_context) + + subject = default_i18n_subject(domain_name: domain.name) + mail(to: send_to, subject: subject) + end + def expired(domain:, registrar:, registrant:, send_to:) @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) diff --git a/app/views/mailers/registrant_change_mailer/cancelled.html.erb b/app/views/mailers/registrant_change_mailer/cancelled.html.erb new file mode 100644 index 0000000000..9e12d4bde3 --- /dev/null +++ b/app/views/mailers/registrant_change_mailer/cancelled.html.erb @@ -0,0 +1,18 @@ +Tere +

+Registripidaja tühistas domeeni <%= @domain.name %> registreerija vahetuse taotluse. Varem saadetud kinnituslink ei kehti enam. +

+Küsimuste korral palun võtke ühendust oma registripidajaga: + +<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> +
+

+Hi, +

+The registrar has cancelled the registrant change request for the domain <%= @domain.name %>. The confirmation link sent earlier is no longer valid. +

+Please contact your registrar if you have any questions: + +<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> diff --git a/app/views/mailers/registrant_change_mailer/cancelled.text.erb b/app/views/mailers/registrant_change_mailer/cancelled.text.erb new file mode 100644 index 0000000000..2a15598ced --- /dev/null +++ b/app/views/mailers/registrant_change_mailer/cancelled.text.erb @@ -0,0 +1,20 @@ +Tere + +Registripidaja tühistas domeeni <%= @domain.name %> registreerija vahetuse taotluse. Varem saadetud kinnituslink ei kehti enam. + +Küsimuste korral palun võtke ühendust oma registripidajaga: + +<%= render 'mailers/shared/registrar/registrar.et.text', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.text' %> + +-------------------------------------- + +Hi, + +The registrar has cancelled the registrant change request for the domain <%= @domain.name %>. The confirmation link sent earlier is no longer valid. + +Please contact your registrar if you have any questions: + +<%= render 'mailers/shared/registrar/registrar.en.text', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.en.text' %> diff --git a/config/locales/mailers/registrant_change.en.yml b/config/locales/mailers/registrant_change.en.yml index 744715807d..e3b8cf820e 100644 --- a/config/locales/mailers/registrant_change.en.yml +++ b/config/locales/mailers/registrant_change.en.yml @@ -19,4 +19,8 @@ en: expired: subject: >- Domeeni %{domain_name} registreerija vahetuse taotlus on tühistatud - / %{domain_name} registrant change cancelled \ No newline at end of file + / %{domain_name} registrant change cancelled + cancelled: + subject: >- + Registripidaja tühistas domeeni %{domain_name} registreerija vahetuse taotluse + / %{domain_name} registrant change request was cancelled by the registrar \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 05af94cda3..19d67731b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,6 +167,7 @@ resources :renew, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/renews' resources :transfer, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/transfers' resources :statuses, only: %i[update destroy], constraints: { id: /.*/ }, controller: 'domains/statuses' + resource :pending_update, only: %i[destroy], controller: 'domains/pending_updates' match 'dnssec', to: 'domains/dnssec#destroy', via: 'delete', defaults: { id: nil } match 'contacts', to: 'domains/contacts#destroy', via: 'delete', defaults: { id: nil } collection do diff --git a/test/integration/epp/domain/update/cancel_pending_test.rb b/test/integration/epp/domain/update/cancel_pending_test.rb new file mode 100644 index 0000000000..957157c969 --- /dev/null +++ b/test/integration/epp/domain/update/cancel_pending_test.rb @@ -0,0 +1,129 @@ +require 'test_helper' + +class EppDomainUpdateCancelPendingTest < EppTestCase + include ActionMailer::TestHelper + include ActiveJob::TestHelper + + setup do + @domain = domains(:shop) + @original_registrant_change_verification = + Setting.request_confirmation_on_registrant_change_enabled + Setting.request_confirmation_on_registrant_change_enabled = true + ActionMailer::Base.deliveries.clear + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + end + + teardown do + Setting.request_confirmation_on_registrant_change_enabled = + @original_registrant_change_verification + end + + def test_cancels_pending_update_when_current_registrant_is_requested_again + request_registrant_change + old_registrant = @domain.registrant + + post_domain_update(old_registrant) + + assert_epp_response :completed_successfully + assert_equal old_registrant, @domain.registrant + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_not @domain.registrant_verification_asked? + end + + def test_notifies_both_registrants_when_pending_update_is_cancelled + request_registrant_change + new_registrant_email = @domain.new_registrant_email + registrant_email = @domain.registrant.email + ActionMailer::Base.deliveries.clear + + perform_enqueued_jobs { post_domain_update(@domain.registrant) } + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, new_registrant_email + assert_includes email.to, registrant_email + end + + def test_rejects_update_of_pending_domain_when_another_registrant_is_requested + request_registrant_change + old_registrant = @domain.registrant + + post_domain_update(contacts(:jack)) + + assert_epp_response :object_status_prohibits_operation + assert_equal old_registrant, @domain.registrant + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def test_does_not_cancel_pending_update_when_other_changes_are_requested + request_registrant_change + old_transfer_code = @domain.transfer_code + + post_domain_update(@domain.registrant, transfer_code: 'new-transfer-code') + + assert_epp_response :object_status_prohibits_operation + assert_equal old_transfer_code, @domain.transfer_code + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def test_keeps_regular_update_intact_when_domain_has_no_pending_update + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + + post_domain_update(@domain.registrant) + + assert_epp_response :completed_successfully + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + private + + def request_registrant_change + new_registrant = contacts(:william) + assert_not_equal new_registrant, @domain.registrant + + post_domain_update(new_registrant) + + assert_epp_response :completed_successfully_action_pending + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def post_domain_update(registrant, transfer_code: nil) + post epp_update_path, + params: { frame: registrant_change_xml(registrant, transfer_code: transfer_code) }, + headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + + # assert_epp_response memoizes the parsed response, reset it between requests + @epp_response = nil + @domain.reload + end + + def registrant_change_xml(registrant, transfer_code: nil) + auth_info = if transfer_code + "#{transfer_code}" + end + + <<-XML + + + + + + #{@domain.name} + + #{registrant.code} + #{auth_info} + + + + + + #{'test' * 2000} + + + + + XML + end +end diff --git a/test/integration/repp/v1/domains/cancel_pending_update_test.rb b/test/integration/repp/v1/domains/cancel_pending_update_test.rb new file mode 100644 index 0000000000..a5474c8634 --- /dev/null +++ b/test/integration/repp/v1/domains/cancel_pending_update_test.rb @@ -0,0 +1,94 @@ +require 'test_helper' + +class ReppV1DomainsCancelPendingUpdateTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + @domain = domains(:shop) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + @auth_headers = { 'Authorization' => "Basic #{token}" } + Setting.request_confirmation_on_registrant_change_enabled = true + end + + def test_cancels_pending_registrant_change + request_registrant_change + + json = cancel_pending_update + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal @domain.name, json[:data][:domain][:name] + refute_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_nil @domain.registrant_verification_token + assert_nil @domain.registrant_verification_asked_at + end + + def test_keeps_current_registrant_on_cancel + old_registrant = @domain.registrant + request_registrant_change + + cancel_pending_update + + assert_equal old_registrant, @domain.registrant + end + + def test_notifies_both_registrants_on_cancel + request_registrant_change + new_registrant_email = @domain.new_registrant_email + registrant_email = @domain.registrant.email + ActionMailer::Base.deliveries.clear + + perform_enqueued_jobs { cancel_pending_update } + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, new_registrant_email + assert_includes email.to, registrant_email + end + + def test_returns_error_when_domain_has_no_pending_update + refute_includes @domain.statuses, DomainStatus::PENDING_UPDATE + + json = cancel_pending_update + + assert_response :bad_request + assert_equal 2304, json[:code] + end + + def test_does_not_cancel_pending_update_of_another_registrar + request_registrant_change + other_user = users(:api_goodnames) + token = Base64.encode64("#{other_user.username}:#{other_user.plain_text_password}") + @auth_headers = { 'Authorization' => "Basic #{token}" } + + json = cancel_pending_update + + assert_response :not_found + assert_equal 2303, json[:code] + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + private + + def request_registrant_change + new_registrant = contacts(:william) + refute_equal new_registrant, @domain.registrant + + put "/repp/v1/domains/#{@domain.name}", + headers: json_headers, + params: { domain: { registrant: { code: new_registrant.code } } }.to_json + + @domain.reload + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def cancel_pending_update + delete "/repp/v1/domains/#{@domain.name}/pending_update", headers: json_headers + + @domain.reload + JSON.parse(response.body, symbolize_names: true) + end + + def json_headers + @auth_headers.merge('Content-Type' => 'application/json') + end +end diff --git a/test/interactions/domains/cancel_pending_update_test.rb b/test/interactions/domains/cancel_pending_update_test.rb new file mode 100644 index 0000000000..a9e8215814 --- /dev/null +++ b/test/interactions/domains/cancel_pending_update_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' + +module Domains + class CancelPendingUpdateTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + include ActiveJob::TestHelper + + setup do + @domain = domains(:shop) + @new_registrant = contacts(:william) + @domain.update!(registrant_verification_asked_at: Time.zone.now, + registrant_verification_token: 'test') + @domain.pending_json = { 'new_registrant_id' => @new_registrant.id, + 'new_registrant_email' => @new_registrant.email, + 'new_registrant_name' => @new_registrant.name } + @domain.statuses = [DomainStatus::PENDING_UPDATE] + @domain.save(validate: false) + ActionMailer::Base.deliveries.clear + end + + def test_clears_pending_update_data + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_nil @domain.registrant_verification_token + assert_nil @domain.registrant_verification_asked_at + assert_equal '', @domain.status_notes[DomainStatus::PENDING_UPDATE] + end + + def test_keeps_registrant_untouched + old_registrant = @domain.registrant + + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_equal old_registrant, @domain.registrant + end + + def test_notifies_both_registrants + registrant_email = @domain.registrant.email + + perform_enqueued_jobs do + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + end + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, @new_registrant.email + assert_includes email.to, registrant_email + end + + def test_updates_whois_record + assert_enqueued_with(job: UpdateWhoisRecordJob, args: [@domain.name, 'domain']) do + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + end + end + + def test_fails_when_domain_has_no_pending_update + @domain.statuses = [] + @domain.save(validate: false) + + result = CancelPendingUpdate.run(domain: @domain, initiator: 'test') + + assert_not result.valid? + assert_no_enqueued_emails + end + + def test_does_not_touch_other_statuses + @domain.statuses = [DomainStatus::PENDING_UPDATE, DomainStatus::CLIENT_HOLD] + @domain.save(validate: false) + + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_includes @domain.statuses, DomainStatus::CLIENT_HOLD + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + end +end diff --git a/test/jobs/domain_update_confirm_job_test.rb b/test/jobs/domain_update_confirm_job_test.rb index 158729ae3c..380ec494e7 100644 --- a/test/jobs/domain_update_confirm_job_test.rb +++ b/test/jobs/domain_update_confirm_job_test.rb @@ -36,7 +36,23 @@ def test_registrant_unlocked_domain assert_equal(@domain.registrar.notifications.last.text, "Domain #{@domain.name} has been unlocked by registrant") end + # The registrant decision may reach us after the registrar has already cancelled + # the pending update, or after the expiry cron has cleaned it up. + def test_skips_confirmation_when_pending_update_is_already_gone + set_pending_update + old_registrant_code = @domain.registrant.code + @domain.update!(statuses: [DomainStatus::OK]) + + assert_no_difference '@domain.registrar.notifications.count' do + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) + end + @domain.reload + + assert_equal old_registrant_code, @domain.registrant.code + end + def test_rejected_registrant_verification_notifies_registrar + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) last_registrar_notification = @domain.registrar.notifications.last @@ -45,6 +61,7 @@ def test_rejected_registrant_verification_notifies_registrar end def test_accepted_registrant_verification_notifies_registrar + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) last_registrar_notification = @domain.registrar.notifications.last @@ -61,6 +78,7 @@ def test_changes_domain_registrant_after_approval @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) + set_pending_update @domain.reload DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @@ -79,6 +97,7 @@ def test_clears_pending_update_after_denial @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload @@ -96,7 +115,8 @@ def test_protects_statuses_after_denial @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload @@ -116,7 +136,8 @@ def test_protects_statuses_after_confirm @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @domain.reload @@ -137,7 +158,8 @@ def test_works_id_current_user_id_broken @domain.pending_json['frame'] = parsed_frame @domain.pending_json['current_user_id'] = { key: 'some_value'} @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) assert_nothing_raised do DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @@ -191,4 +213,12 @@ def test_clears_pending_update_and_sets_ok_after_denial assert_not @domain.statuses.include? DomainStatus::PENDING_UPDATE assert @domain.statuses.include? DomainStatus::OK end + + private + + def set_pending_update + @domain.statuses = [DomainStatus::PENDING_UPDATE] + @domain.save(validate: false) + @domain.reload + end end