diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index d50636e26..f4498f4da 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -6,7 +6,7 @@ jobs: test: services: postgres: - image: postgres:12 + image: postgres:14 ports: ["5432:5432"] env: POSTGRES_PASSWORD: password @@ -79,7 +79,7 @@ jobs: - name: Save coverage run: ./cc-test-reporter format-coverage --output coverage/codeclimate.${{ matrix.ruby }}.json - - uses: actions/upload-artifact@v3.0.0 + - uses: actions/upload-artifact@v3.1.0 with: name: coverage-${{ matrix.ruby }} path: coverage/codeclimate.${{ matrix.ruby }}.json diff --git a/.gitignore b/.gitignore index 1c4a85f46..a8499459f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,13 @@ /coverage/ /.bundle /vendor/bundle +/vendor/gems /config/database.yml /config/application.yml /config/environments/development.rb /config/deploy.rb /config/master.key /.idea - +/config/master.key # Do not commit one. Instead, download the latest from https://github.com/internetee/style-guide. .rubocop.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index ec57fcc40..a9b96159e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,89 @@ +08.09.2022 +* Fixed template error for multi-year registered domains in force delete process [#2435](https://github.com/internetee/registry/issues/2435) + +02.09.2022 +* Update invoice status on payment order payments [#2427](https://github.com/internetee/registry/pull/2427) + +01.09.2022 +* Monthly invoice payment status fix [#2428](https://github.com/internetee/registry/issues/2428) + +31.08.2022 +* new fully automated process for registrar monthly invoices [#2424](https://github.com/internetee/registry/pull/2424) + +25.08.2022 +* Contact creation fix to not require postal addresses in Registrar portal [#2421](https://github.com/internetee/registry/pull/2421) + +23.08.2022 +* REPP update to fix search by registrant in Registrar portal [#2425](https://github.com/internetee/registry/pull/2425) + +21.07.2022 +* Removed deprecated statuses_before_force_delete field [#2363](https://github.com/internetee/registry/issues/2363) + +15.07.2022 +* REPP api update for new registrar portal [#2387](https://github.com/internetee/registry/pull/2387) + +4.07.2022 +* Update apipie-rails to 0.8.0 [#2383](https://github.com/internetee/registry/pull/2383) +* Bump jmespath to 1.6.1 [#2388](https://github.com/internetee/registry/pull/2388) + +1.07.2022 +* Update pg to 1.4.1 [#2394](https://github.com/internetee/registry/pull/2394) +* Update rack to 2.2.4 [#2398](https://github.com/internetee/registry/pull/2398) + +21.06.2022 +* Update pg to 1.4.0 [#2392](https://github.com/internetee/registry/pull/2392) + +02.06.2022 +* fix for force delete check query [#2380](https://github.com/internetee/registry/pull/2380) +* Integration with the billing service [#2266](https://github.com/internetee/registry/pull/2266) + +25.05.2022 +* Fixed looping validation issue [#2377](https://github.com/internetee/registry/pull/2377) +* ForceDelete query fix [#2380](https://github.com/internetee/registry/pull/2380) + +19.05.2022 +* Process to remove expired validation event records [#2236](https://github.com/internetee/registry/issues/2236) + +17.05.2022 +* removed unnecessary contact validation on contact create [#2376](https://github.com/internetee/registry/pull/2376) +* Refactored email validation job [#2369](https://github.com/internetee/registry/pull/2369) +* Job for deprecated validation events removal [#2374](https://github.com/internetee/registry/issues/2374) + +09.05.2022 +* test for auction view [#2373](https://github.com/internetee/registry/pull/2373) + +06.05.2022 +* refactored out the contact_code_cache from domain_contacts model [#2370](https://github.com/internetee/registry/issues/2370) + +28.04.2022 +* Fixed ns and dnssec validation error messages [#2296](https://github.com/internetee/registry/issues/2296) +* Added status notes to REPP domain info output [#2331](https://github.com/internetee/registry/issues/2331) +* Added auction list view to admin for improved ahandling of upcoming enlgish auction feature [#2341](https://github.com/internetee/registry/pull/2341) + +27.04.2022 +* Refactored email validation - reducing dns requests [#2364](https://github.com/internetee/registry/issues/2364) + +21.04.2022 +* Delay renovate Ruby version updates for 60 days [#2361](https://github.com/internetee/registry/issues/2361) + +20.04.2022 +* Contacts with disclosed attributes can now be updated [#2340](https://github.com/internetee/registry/issues/2340) +* Legacy code fix [#2360](https://github.com/internetee/registry/pull/2360) + +19.04.2022 +* Rolled back ruby version to 3.0.3 [#2358](https://github.com/internetee/registry/pull/2358) + +18.04.2022 +* Fixed error 2005 epp syntax issue [#2338](https://github.com/internetee/registry/issues/2338) +* Fixed poll issue with email validations [#2343](https://github.com/internetee/registry/issues/2343) +* Removed registrant portal code from registry project [#2350](https://github.com/internetee/registry/issues/2350) + +14.04.2022 +* Removed legacy email verification code [#2349](https://github.com/internetee/registry/issues/2349) + +06.04.2022 +* Contact email validation on domain update [#2213](https://github.com/internetee/registry/issues/2213) + 05.04.2022 * Automatic contact name update poll messages are now grouped together into one change poll message [#2307](https://github.com/internetee/registry/issues/2307) * Status notes are now added to status elements of epp xml [#2211](https://github.com/internetee/registry/issues/2211) diff --git a/Dockerfile b/Dockerfile index 9c46182a3..3d065e5bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM internetee/ruby:3.0-buster 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 EXPOSE 3000 diff --git a/Gemfile b/Gemfile index 11f1da423..4ba187b94 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' # core gem 'active_interaction', '~> 4.0' -gem 'apipie-rails', '~> 0.5.19' +gem 'apipie-rails', '~> 0.6.0' gem 'bootsnap', '>= 1.1.0', require: false gem 'iso8601', '0.13.0' # for dates and times gem 'mimemagic', '0.4.3' @@ -17,11 +17,11 @@ gem 'figaro', '~> 1.2' # model related gem 'paper_trail', '~> 12.1' -gem 'pg', '1.3.5' +gem 'pg', '1.4.3' # 1.8 is for Rails < 5.0 gem 'ransack', '~> 2.6.0' gem 'truemail', '~> 2.4' # validates email by regexp, mail server existence and address existence -gem 'validates_email_format_of', '1.6.3' # validates email against RFC 2822 and RFC 3696 +gem 'validates_email_format_of', '1.7.2' # validates email against RFC 2822 and RFC 3696 # 0.7.3 is the latest for Rails 4.2, however, it is absent on Rubygems server # https://github.com/huacnlee/rails-settings-cached/issues/165 @@ -57,10 +57,9 @@ gem 'digidoc_client', ref: '1645e83a5a548addce383f75703b0275c5310c32' # TARA -gem 'omniauth' gem 'omniauth-rails_csrf_protection' gem 'omniauth-tara', github: 'internetee/omniauth-tara' - +# gem 'omniauth-tara', path: 'vendor/gems/omniauth-tara' gem 'airbrake' gem 'epp', github: 'internetee/epp', branch: :master @@ -73,11 +72,12 @@ gem 'company_register', github: 'internetee/company_register', branch: 'master' gem 'domain_name' gem 'e_invoice', github: 'internetee/e_invoice', branch: :master -gem 'haml', '~> 5.2' +gem 'haml', '~> 6.0' gem 'lhv', github: 'internetee/lhv', branch: 'master' gem 'rexml' gem 'wkhtmltopdf-binary', '~> 0.12.5.1' + gem 'directo', github: 'internetee/directo', branch: 'master' group :development, :test do @@ -101,3 +101,6 @@ gem 'pghero' gem 'pg_query', '>= 0.9.0' gem 'newrelic_rpm' gem 'newrelic-infinite_tracing' + +# token +gem 'jwt' diff --git a/Gemfile.lock b/Gemfile.lock index 19679d42e..10e0805f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,10 +18,10 @@ GIT GIT remote: https://github.com/internetee/e_invoice.git - revision: 312cac173935f434e449d1714f3497bfee9f8995 + revision: 9f850465697a2448a31ebddb83c1be5a5a9be3d2 branch: master specs: - e_invoice (0.1.0) + e_invoice (0.1.3) builder (~> 3.2) nokogiri savon @@ -149,8 +149,9 @@ GEM akami (1.3.1) gyoku (>= 0.4.0) nokogiri - apipie-rails (0.5.19) - rails (>= 4.1) + apipie-rails (0.6.0) + actionpack (>= 4.1) + activesupport (>= 4.1) attr_required (1.0.1) autoprefixer-rails (10.2.4.0) execjs @@ -240,8 +241,9 @@ GEM googleapis-common-protos-types (~> 1.0) gyoku (1.3.1) builder (>= 2.1.2) - haml (5.2.2) - temple (>= 0.8.0) + haml (6.0.0) + temple (>= 0.8.2) + thor tilt hashdiff (1.0.1) hashie (4.1.0) @@ -253,12 +255,12 @@ GEM httpi (2.4.5) rack socksify - i18n (1.10.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) i18n_data (0.13.0) isikukood (0.1.2) iso8601 (0.13.0) - jmespath (1.4.0) + jmespath (1.6.1) jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -270,6 +272,7 @@ GEM activesupport (>= 4.2) aes_key_wrap bindata + jwt (2.3.0) kaminari (1.2.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.1) @@ -284,7 +287,7 @@ GEM kaminari-core (1.2.1) libxml-ruby (3.2.1) logger (1.4.3) - loofah (2.16.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -316,10 +319,10 @@ GEM newrelic_rpm (= 8.1.0) newrelic_rpm (8.1.0) nio4r (2.5.8) - nokogiri (1.13.3) + nokogiri (1.13.6) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.3-x86_64-linux) + nokogiri (1.13.6-x86_64-linux) racc (~> 1.4) nori (2.6.0) omniauth (1.9.1) @@ -328,7 +331,7 @@ GEM omniauth-rails_csrf_protection (0.1.2) actionpack (>= 4.2) omniauth (>= 1.3.1) - openid_connect (1.2.0) + openid_connect (1.3.0) activemodel attr_required (>= 1.0.0) json-jwt (>= 1.5.0) @@ -343,7 +346,7 @@ GEM activerecord (>= 5.2) request_store (~> 1.1) pdfkit (0.8.5) - pg (1.3.5) + pg (1.4.3) pg_query (2.1.2) google-protobuf (>= 3.17.1) pghero (2.8.1) @@ -355,13 +358,15 @@ GEM puma (5.6.4) nio4r (~> 2.0) racc (1.6.0) - rack (2.2.3) + rack (2.2.4) rack-oauth2 (1.16.0) activesupport attr_required httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) + rack-protection (2.2.0) + rack rack-test (1.1.0) rack (>= 1.0, < 3) rails (6.1.4.1) @@ -382,7 +387,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) + rails-html-sanitizer (1.4.3) loofah (~> 2.3) railties (6.1.4.1) actionpack (= 6.1.4.1) @@ -409,6 +414,7 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) rexml (3.2.5) + ruby2_keywords (0.0.5) rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -444,6 +450,11 @@ GEM simplecov-html (0.10.2) simpleidn (0.2.1) unf (~> 0.1.4) + sinatra (2.2.0) + mustermann (~> 1.0) + rack (~> 2.2) + rack-protection (= 2.2.0) + tilt (~> 2.0) sixarm_ruby_unaccent (1.2.0) socksify (1.7.1) sprockets (4.0.2) @@ -454,13 +465,13 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) spy (1.0.1) - swd (1.2.0) + swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) temple (0.8.2) thor (1.2.1) - tilt (2.0.10) + tilt (2.0.11) truemail (2.4.9) simpleidn (~> 0.2.1) tzinfo (2.0.4) @@ -473,10 +484,10 @@ GEM validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) - validate_url (1.0.13) + validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix - validates_email_format_of (1.6.3) + validates_email_format_of (1.7.2) i18n warden (1.2.9) rack (>= 2.0.9) @@ -488,7 +499,7 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (>= 3.0, < 4.0) - webfinger (1.1.0) + webfinger (1.2.0) activesupport httpclient (>= 2.4) webmock (3.14.0) @@ -512,7 +523,7 @@ PLATFORMS DEPENDENCIES active_interaction (~> 4.0) airbrake - apipie-rails (~> 0.5.19) + apipie-rails (~> 0.6.0) aws-sdk-sesv2 (~> 1.19) bootsnap (>= 1.1.0) bootstrap-sass (~> 3.4) @@ -533,11 +544,12 @@ DEPENDENCIES epp! epp-xml (= 1.2.0)! figaro (~> 1.2) - haml (~> 5.2) + haml (~> 6.0) isikukood iso8601 (= 0.13.0) jquery-rails jquery-ui-rails (= 6.0.1) + jwt kaminari lhv! mime-types-data @@ -547,12 +559,11 @@ DEPENDENCIES newrelic-infinite_tracing newrelic_rpm nokogiri (~> 1.13.0) - omniauth omniauth-rails_csrf_protection omniauth-tara! paper_trail (~> 12.1) pdfkit - pg (= 1.3.5) + pg (= 1.4.3) pg_query (>= 0.9.0) pghero pry (= 0.14.1) @@ -570,11 +581,11 @@ DEPENDENCIES spy truemail (~> 2.4) uglifier - validates_email_format_of (= 1.6.3) + validates_email_format_of (= 1.7.2) webdrivers webmock whenever (= 1.0.0) wkhtmltopdf-binary (~> 0.12.5.1) BUNDLED WITH - 2.3.10 + 2.3.21 diff --git a/app/assets/images/everypay.png b/app/assets/images/everypay.png new file mode 100644 index 000000000..c1456c4be Binary files /dev/null and b/app/assets/images/everypay.png differ diff --git a/app/assets/stylesheets/registrant-manifest.sass b/app/assets/stylesheets/registrant-manifest.sass deleted file mode 100644 index 6d0a281fe..000000000 --- a/app/assets/stylesheets/registrant-manifest.sass +++ /dev/null @@ -1,11 +0,0 @@ -//= require 'registrant/registrant-bootstrap' -//= require 'jquery-ui/datepicker' -//= require 'select2' -//= require 'select2-bootstrap' -@import shared/fonts -@import shared/general -@import forms -@import typeaheadjs -@import selectize -@import selectize.bootstrap3 -@import registrant/registrant diff --git a/app/assets/stylesheets/registrant/registrant-bootstrap.sass b/app/assets/stylesheets/registrant/registrant-bootstrap.sass deleted file mode 100644 index 08f6eb984..000000000 --- a/app/assets/stylesheets/registrant/registrant-bootstrap.sass +++ /dev/null @@ -1,19 +0,0 @@ -$brand-primary: #7EA82F -$navbar-default-bg: #7EA82F -$navbar-default-brand-color: #fff -$navbar-default-link-color: #fff -$border-radius-base: 2px -$body-bg: #F8F8F8 -$container-large-desktop: 1040px -$font-family-sans-serif: 'EtelkaLightProRegular', Arial, Helvetica, sans-serif -$font-family-serif: 'EtelkaLightProBold', Georgia, "Times New Roman", Times, serif -$font-size-h1: 26px -$navbar-default-link-active-color: #333 - -@import 'bootstrap-sprockets' -@import 'bootstrap' -@import 'shared/general-bootstrap' - -// Support rails error element -.field_with_errors - @extend .has-error diff --git a/app/assets/stylesheets/registrant/registrant.sass b/app/assets/stylesheets/registrant/registrant.sass deleted file mode 100644 index ebe9f4974..000000000 --- a/app/assets/stylesheets/registrant/registrant.sass +++ /dev/null @@ -1,44 +0,0 @@ -html - position: relative - min-height: 100% - overflow-y: scroll - -body - padding-bottom: 130px - -body > .container - height: 100% - background: #fff - padding: 60px 30px 30px 30px - -h1, h2, h3, h4 - margin-bottom: 0px !important - -// Commented out, default 20px is needed on forms -// hr - // margin-top: 10px !important - // margin-bottom: 10px !important - -.navbar li - font-weight: bold - -.footer - position: absolute - bottom: 0 - width: 100% - height: 130px - background: image_url('bg.jpg') - color: white !important - background-size: 100% - -.confirmation - padding: 40px 0 20px 0 - .column-keys - text-align: right - width: 49% - float: left - .column-values - float: right - font-weight: bold - text-align: left - width: 49% diff --git a/app/controllers/admin/auctions_controller.rb b/app/controllers/admin/auctions_controller.rb new file mode 100644 index 000000000..c1023b705 --- /dev/null +++ b/app/controllers/admin/auctions_controller.rb @@ -0,0 +1,142 @@ +module Admin + class AuctionsController < BaseController + load_and_authorize_resource + + def index + params[:q] ||= {} + + @auctions = Auction.with_domain_name(params[:domain_matches]) + .with_status(params[:statuses_contains]) + .with_start_created_at_date(params[:created_at_start]) + .with_end_created_at_date(params[:created_at_end]) + .order(created_at: :desc) + + @auction = Auction.new + + normalize_search_parameters do + @q = @auctions.ransack(PartialSearchFormatter.format(params[:q])) + @auctions = @q.result.page(params[:page]) + end + + @auctions = @auctions.per(params[:results_per_page_auction]) if params[:results_per_page_auction].to_i.positive? + + domains = ReservedDomain.all.order(:name) + q = domains.ransack(PartialSearchFormatter.format(params[:q])) + @domains = q.result.page(params[:page]) + @domains = @domains.per(params[:results_per_page]) if params[:results_per_page].to_i.positive? + + render_by_format('admin/auctions/index', 'auctions') + end + + def create + auction = Auction.new(domain: params[:domain], status: Auction.statuses[:started], platform: 'manual') + + if domain_exists_in_blocked_disputed_and_registered?(params[:domain]) + flash[:alert] = "Adding #{params[:domain]} failed - domain registered or regsitration is blocked" + redirect_to admin_auctions_path and return + end + + result = check_availability(params[:domain])[0] + if result[:avail].zero? + flash[:alert] = "Cannot generate domain. Reason: #{result[:reason]}" + redirect_to admin_auctions_path and return + end + + if auction.save + reserved_domain = auction.domain if remove_from_reserved(auction) + flash[:notice] = "Auction #{params[:domain]} created. + #{reserved_domain.present? ? 'These domain will be removed from reserved list: ' + reserved_domain : ' '}" + else + flash[:alert] = 'Something goes wrong' + end + + redirect_to admin_auctions_path + end + + def upload_spreadsheet + if params[:q].nil? + flash[:alert] = 'No file upload! Look at the left of upload button!' + redirect_to admin_auctions_path and return + end + + filename = params[:q][:file] + table = CSV.parse(File.read(filename), headers: true) + + failed_names = [] + reserved_domains = [] + + if validate_table(table) + table.each do |row| + record = row.to_h + + if domain_exists_in_blocked_disputed_and_registered?(record['name']) + failed_names << record['name'] + + next + end + + result = check_availability(record['name'])[0] + if result[:avail].zero? + failed_names << record['name'] + + next + end + + auction = Auction.new(domain: record['name'], status: Auction.statuses[:started], platform: 'manual') + flag = remove_from_reserved(auction) if auction.save! + reserved_domains << auction.domain if flag + end + + message_template = "Domains added! + #{reserved_domains.present? ? + 'These domains will be removed from reserved list: ' + reserved_domains.join(' ') + '! ' + : '! '} + #{failed_names.present? ? 'These domains were ignored: ' + failed_names.join(' ') : '!'}" + + flash[:notice] = message_template + else + flash[:alert] = "Invalid CSV format. Should be column with 'name' where is the list of name of domains!" + end + + redirect_to admin_auctions_path + end + + private + + def check_availability(domain_name) + Epp::Domain.check_availability(domain_name) + end + + def domain_exists_in_blocked_disputed_and_registered?(domain_name) + Domain.exists?(name: domain_name) || + BlockedDomain.exists?(name: domain_name) || + Dispute.exists?(domain_name: domain_name) || + Auction.exists?(domain: domain_name) + end + + def validate_table(table) + first_row = table.headers + first_row.include? 'name' + end + + def remove_from_reserved(auction) + domain = ReservedDomain.find_by(name: auction.domain) + + domain.destroy if domain.present? + end + + def normalize_search_parameters + ca_cache = params[:q][:valid_to_lteq] + begin + end_time = params[:q][:valid_to_lteq].try(:to_date) + params[:q][:valid_to_lteq] = end_time.try(:end_of_day) + rescue + logger.warn('Invalid date') + end + + yield + + params[:q][:valid_to_lteq] = ca_cache + end + end +end diff --git a/app/controllers/admin/contacts_controller.rb b/app/controllers/admin/contacts_controller.rb index 955ab4df2..73ead7b25 100644 --- a/app/controllers/admin/contacts_controller.rb +++ b/app/controllers/admin/contacts_controller.rb @@ -32,7 +32,6 @@ module Admin contacts = contacts.where("ident_country_code is null or ident_country_code=''") end - contacts = contacts.email_verification_failed if params[:email_verification_failed].eql?('1') contacts end diff --git a/app/controllers/admin/invoices_controller.rb b/app/controllers/admin/invoices_controller.rb index 223257605..a4ba97310 100644 --- a/app/controllers/admin/invoices_controller.rb +++ b/app/controllers/admin/invoices_controller.rb @@ -13,6 +13,7 @@ module Admin if @invoice&.persisted? flash[:notice] = t(:record_created) + # send_invoice_data_to_billing_system redirect_to [:admin, @invoice] else flash.now[:alert] = t(:failed_to_create_record) @@ -48,6 +49,8 @@ module Admin def cancel @invoice.cancel + EisBilling::SendInvoiceStatus.send_info(invoice_number: @invoice.number, status: 'cancelled') + redirect_to [:admin, @invoice], notice: t('.cancelled') end @@ -78,18 +81,24 @@ module Admin payment_order.update(notes: 'Cancelled') end + # rubocop:disable Metrics/MethodLength def filter_by_status case params[:status] when 'Paid' Invoice.includes(:account_activity, :buyer).where.not(account_activity: { id: nil }) when 'Unpaid' - Invoice.includes(:account_activity, :buyer).where(account_activity: { id: nil }) + Invoice.includes(:account_activity, :buyer).where(account_activity: { id: nil }, + cancelled_at: nil, + monthly_invoice: false) when 'Cancelled' Invoice.includes(:account_activity, :buyer).where.not(cancelled_at: nil) + when 'Monthly' + Invoice.where(monthly_invoice: true, cancelled_at: nil) else Invoice.includes(:account_activity, :buyer) end end + # rubocop:enable Metrics/MethodLength def filter_by_receipt_date(invoices) date_from_param = params[:q][:receipt_date_gteq] if params[:q][:receipt_date_gteq].present? diff --git a/app/controllers/admin/reserved_domains_controller.rb b/app/controllers/admin/reserved_domains_controller.rb index 1bfade83e..20957dec4 100644 --- a/app/controllers/admin/reserved_domains_controller.rb +++ b/app/controllers/admin/reserved_domains_controller.rb @@ -51,8 +51,26 @@ module Admin redirect_to admin_reserved_domains_path end + def release_to_auction + redirect_to admin_reserved_domains_path and return if params[:reserved_elements].nil? + + reserved_domains_ids = params[:reserved_elements][:domain_ids] + reserved_domains = ReservedDomain.where(id: reserved_domains_ids) + + reserved_domains.each do |domain| + Auction.create!(domain: domain.name, status: Auction.statuses[:started], platform: 'manual') + domain.destroy! + end + + redirect_to admin_auctions_path + end + private + def reserved_checked_elements + # params.require(:reserved_elements).permit(:name, :password) + end + def reserved_domain_params params.require(:reserved_domain).permit(:name, :password) end diff --git a/app/controllers/api/v1/auctions_controller.rb b/app/controllers/api/v1/auctions_controller.rb index de8e94442..9a01f4e68 100644 --- a/app/controllers/api/v1/auctions_controller.rb +++ b/app/controllers/api/v1/auctions_controller.rb @@ -44,7 +44,7 @@ module Api private def serializable_hash(auction) - { id: auction.uuid, domain: auction.domain, status: auction.status } + { id: auction.uuid, domain: auction.domain, status: auction.status, platform: auction.platform } end def serializable_hash_for_update_action(auction) diff --git a/app/controllers/eis_billing/base_controller.rb b/app/controllers/eis_billing/base_controller.rb new file mode 100644 index 000000000..ad2b71f3a --- /dev/null +++ b/app/controllers/eis_billing/base_controller.rb @@ -0,0 +1,52 @@ +module EisBilling + class BaseController < ApplicationController + protect_from_forgery with: :null_session + skip_authorization_check # Temporary solution + # skip_before_action :verify_authenticity_token # Temporary solution + before_action :authorized + + INITIATOR = 'billing'.freeze + + def encode_token(payload) + JWT.encode(payload, ENV['secret_word']) + end + + def auth_header + # { Authorization: 'Bearer ' } + request.headers['Authorization'] + end + + def decoded_token + return unless auth_header + + token = auth_header.split(' ')[1] + begin + JWT.decode(token, billing_secret_key, true, algorithm: 'HS256') + rescue JWT::DecodeError + nil + end + end + + def accessable_service + return decoded_token[0]['initiator'] == INITIATOR if decoded_token + + false + end + + def logged_in? + !!accessable_service + end + + def authorized + render json: { message: 'Access denied' }, status: :unauthorized unless logged_in? + end + + def billing_secret_key + ENV['billing_secret'] + end + + def logger + Rails.logger + end + end +end diff --git a/app/controllers/eis_billing/directo_response_controller.rb b/app/controllers/eis_billing/directo_response_controller.rb new file mode 100644 index 000000000..9549ec0a6 --- /dev/null +++ b/app/controllers/eis_billing/directo_response_controller.rb @@ -0,0 +1,36 @@ +class EisBilling::DirectoResponseController < EisBilling::BaseController + def update + response = params[:response] + xml_data = params[:xml_data] + @month = params.fetch(:month, false) + + process_directo_response(xml_data, response) + render status: :ok, json: { messege: 'Should return new directo number' } + end + + private + + def process_directo_response(xml, req) + Rails.logger.info "[Directo] - Responded with body: #{xml}" + Nokogiri::XML(req).css('Result').each do |res| + if @month + mark_invoice_as_sent(res: res, req: req) + else + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + + mark_invoice_as_sent(invoice: inv, res: res, req: req) + end + end + end + + def mark_invoice_as_sent(invoice: nil, res:, req:) + directo_record = Directo.new(response: res.as_json.to_h, + request: req, invoice_number: res.attributes['docid'].value.to_i) + if invoice + directo_record.item = invoice + invoice.update(in_directo: true) + end + + directo_record.save! + end +end diff --git a/app/controllers/eis_billing/e_invoice_response_controller.rb b/app/controllers/eis_billing/e_invoice_response_controller.rb new file mode 100644 index 000000000..208c8864f --- /dev/null +++ b/app/controllers/eis_billing/e_invoice_response_controller.rb @@ -0,0 +1,15 @@ +class EisBilling::EInvoiceResponseController < EisBilling::BaseController + def update + invoice_number = params[:invoice_number] + + mark_e_invoice_sent_at(invoice_number) + render status: :ok, json: { message: 'Response received' } + end + + private + + def mark_e_invoice_sent_at(invoice_number) + invoice = Invoice.find_by(number: invoice_number) + invoice.update(e_invoice_sent_at: Time.zone.now) + end +end diff --git a/app/controllers/eis_billing/lhv_connect_transactions_controller.rb b/app/controllers/eis_billing/lhv_connect_transactions_controller.rb new file mode 100644 index 000000000..2e5996881 --- /dev/null +++ b/app/controllers/eis_billing/lhv_connect_transactions_controller.rb @@ -0,0 +1,54 @@ +module EisBilling + class LhvConnectTransactionsController < EisBilling::BaseController + def create + if params['_json'].nil? || params['_json'].empty? + render json: { message: 'MISSING PARAMS' }, status: :unprocessable_entity + return + end + + bank_statement = BankStatement.create(bank_code: Setting.registry_bank_code, + iban: Setting.registry_iban) + + params['_json'].each do |incoming_transaction| + process_transactions(incoming_transaction, bank_statement) + end + + render status: :ok, json: { message: 'RECEIVED', params: params } + end + + private + + def process_transactions(incoming_transaction, bank_statement) + logger.info 'Got incoming transactions' + logger.info incoming_transaction + + ActiveRecord::Base.transaction do + transaction = bank_statement.bank_transactions + .create!(transaction_attributes(incoming_transaction)) + + next if transaction.registrar.blank? + + create_invoice_if_missing(transaction) unless transaction.non_canceled? + end + end + + def create_invoice_if_missing(transaction) + Invoice.create_from_transaction!(transaction) unless transaction.autobindable? + invoice = transaction.autobind_invoice + return unless invoice.paid? + + EisBilling::SendInvoiceStatus.send_info(invoice_number: invoice.number, + status: 'paid') + end + + def transaction_attributes(incoming_transaction) + { + sum: incoming_transaction['amount'], + currency: incoming_transaction['currency'], + paid_at: incoming_transaction['date'], + reference_no: incoming_transaction['payment_reference_number'], + description: incoming_transaction['payment_description'], + } + end + end +end diff --git a/app/controllers/eis_billing/payment_status_controller.rb b/app/controllers/eis_billing/payment_status_controller.rb new file mode 100644 index 000000000..4fa626e5d --- /dev/null +++ b/app/controllers/eis_billing/payment_status_controller.rb @@ -0,0 +1,57 @@ +module EisBilling + class PaymentStatusController < EisBilling::BaseController + TYPE = 'PaymentOrders::EveryPay'.freeze + + def update + payment_status = define_payment_status(params[:payment_state]) + invoice = Invoice.find_by(number: params[:order_reference]) + + return if invoice.paid? + + bank = create_bank_transfer(invoice: invoice, sum: params[:standing_amount], paid_at: params[:transaction_time]) + create_payment_order(invoice: invoice, everypay_response: params, payment_status: payment_status) + + registrar = invoice.buyer + bank.create_activity(registrar, invoice) + + respond_to do |format| + format.json do + render status: :ok, content_type: 'application/json', layout: false, json: { message: 'ok' } + end + end + end + + private + + def define_payment_status(status) + return :paid if PaymentOrders::EveryPay::SUCCESSFUL_PAYMENT.include? status + + :failed + end + + def create_payment_order(invoice:, everypay_response:, payment_status:) + payment = PaymentOrder.new + payment.type = TYPE + payment.invoice = invoice + payment.response = everypay_response + payment.status = payment_status + payment.save + + payment + end + + def create_bank_transfer(invoice:, sum:, paid_at:) + bank = BankTransaction.new + bank.description = invoice.order + bank.reference_no = invoice.reference_no + bank.currency = invoice.currency + bank.iban = invoice.seller_iban + bank.sum = sum + bank.paid_at = paid_at + bank.buyer_name = invoice.buyer_name + bank.save + + bank + end + end +end diff --git a/app/controllers/epp/base_controller.rb b/app/controllers/epp/base_controller.rb index 44e38fe1c..12efbd1d3 100644 --- a/app/controllers/epp/base_controller.rb +++ b/app/controllers/epp/base_controller.rb @@ -168,7 +168,7 @@ module Epp epp_errors.add(:epp_errors, code: '2003', - message: I18n.t('errors.messages.required_parameter_missing', + msg: I18n.t('errors.messages.required_parameter_missing', key: "#{full_selector} [#{attr}]")) end diff --git a/app/controllers/registrant_controller.rb b/app/controllers/registrant_controller.rb deleted file mode 100644 index 1e97281e7..000000000 --- a/app/controllers/registrant_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -class RegistrantController < ApplicationController - before_action :authenticate_registrant_user! - before_action :set_paper_trail_whodunnit - layout 'registrant/application' - - include Registrant::ApplicationHelper - - helper_method :head_title_sufix - - def head_title_sufix - t(:registrant_head_title_sufix) - end - - private - - def current_ability - @current_ability ||= Ability.new(current_registrant_user, request.remote_ip) - end - - def user_for_paper_trail - current_registrant_user.present? ? current_registrant_user.id_role_username : 'anonymous' - end - - def current_user_contacts - current_registrant_user.contacts - rescue CompanyRegister::NotAvailableError - flash.now[:notice] = t('registrant.company_register_unavailable') - current_registrant_user.direct_contacts - end - - def current_user_domains - current_registrant_user.domains - rescue CompanyRegister::NotAvailableError - flash.now[:notice] = t('registrant.company_register_unavailable') - current_registrant_user.direct_domains - end -end diff --git a/app/controllers/registrar/domains_controller.rb b/app/controllers/registrar/domains_controller.rb index e5ab59fa2..5168d25b8 100644 --- a/app/controllers/registrar/domains_controller.rb +++ b/app/controllers/registrar/domains_controller.rb @@ -36,14 +36,12 @@ class Registrar end end - @domains = @domains.per(params[:results_per_page]) if params[:results_per_page].to_i.positive? - respond_to do |format| format.html format.csv do domain_presenters = [] - @domains.find_each do |domain| + @q.result.find_each do |domain| domain_presenters << ::DomainPresenter.new(domain: domain, view: view_context) end @@ -88,14 +86,16 @@ class Registrar @domain_params[:period] = Depp::Domain.default_period end + # rubocop:disable Metrics/CognitiveComplexity def create authorize! :create, Depp::Domain @domain_params = domain_params.to_h @data = @domain.create(@domain_params) - if response_ok? + if @data && response_ok? redirect_to info_registrar_domains_url(domain_name: @domain_params[:name]) else + flash[:alert] = t('.email_error_message') unless @emails_check_result render 'new' end end @@ -113,13 +113,15 @@ class Registrar @data = @domain.update(@domain_params) @dispute = Dispute.active.find_by(domain_name: @domain_params[:name]) - if response_ok? + if @data && response_ok? redirect_to info_registrar_domains_url(domain_name: @domain_params[:name]) else + flash[:alert] = t('.email_error_message') unless @emails_check_result params[:domain_name] = @domain_params[:name] render 'new' end end + # rubocop:enable Metrics/CognitiveComplexity def delete authorize! :delete, Depp::Domain diff --git a/app/controllers/registrar/invoices_controller.rb b/app/controllers/registrar/invoices_controller.rb index 34f6c17ae..2a17b72b0 100644 --- a/app/controllers/registrar/invoices_controller.rb +++ b/app/controllers/registrar/invoices_controller.rb @@ -17,6 +17,8 @@ class Registrar def cancel @invoice.cancel + EisBilling::SendInvoiceStatus.send_info(invoice_number: @invoice.number, status: 'cancelled') + redirect_to [:registrar, @invoice], notice: t('.cancelled') end diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb index 388bc9a94..8395db42c 100644 --- a/app/controllers/repp/v1/accounts_controller.rb +++ b/app/controllers/repp/v1/accounts_controller.rb @@ -1,32 +1,164 @@ module Repp module V1 - class AccountsController < BaseController - api :GET, '/repp/v1/accounts/balance' + class AccountsController < BaseController # rubocop:disable Metrics/ClassLength + load_and_authorize_resource + + api :get, '/repp/v1/accounts' + desc 'Get all activities' + def index + records = current_user.registrar.cash_account.activities + + q = records.ransack(PartialSearchFormatter.format(search_params)) + q.sorts = 'created_at desc' if q.sorts.empty? + activities = q.result(distinct: true) + + limited_activities = activities.limit(limit).offset(offset) + .includes(:invoice) + + render_success(data: { activities: serialized_activities(limited_activities), + count: activities.count, + types_for_select: AccountActivity.types_for_select }) + end + + api :get, '/repp/v1/accounts/details' + desc 'Get current registrar account details' + def details + registrar = current_user.registrar + type = registrar.settings['balance_auto_reload']&.dig('type') + resp = { account: { billing_email: registrar.billing_email, + iban: registrar.iban, + iban_max_length: Iban.max_length, + linked_users: serialized_users(current_user.linked_users), + balance_auto_reload: type, + min_deposit: Setting.minimum_deposit } } + render_success(data: resp) + end + + api :put, '/repp/v1/accounts' + desc 'Update current registrar account details' + def update + registrar = current_user.registrar + unless registrar.update(account_params) + handle_non_epp_errors(registrar) + return + end + + render_success(data: { account: account_params }, + message: I18n.t('registrar.account.update.saved')) + end + + api :post, '/repp/v1/accounts/update_auto_reload_balance' + desc 'Enable current registrar balance auto reload' + def update_auto_reload_balance + type = BalanceAutoReloadTypes::Threshold.new(type_params) + unless type.valid? + handle_non_epp_errors(type) + return + end + + settings = { balance_auto_reload: { type: type.as_json } } + current_user.registrar.update!(settings: settings) + render_success(data: { settings: settings }, + message: I18n.t('registrar.settings.balance_auto_reload.update.saved')) + end + + api :get, '/repp/v1/accounts/disable_auto_reload_balance' + desc 'Disable current registrar balance auto reload' + def disable_auto_reload_balance + registrar = current_user.registrar + registrar.settings.delete('balance_auto_reload') + registrar.save! + + render_success(data: { settings: registrar.settings }, + message: I18n.t('registrar.settings.balance_auto_reload.destroy.disabled')) + end + + api :put, '/repp/v1/accounts/switch_user' + desc 'Switch user to another api user' + def switch_user + new_user = ApiUser.find(account_params[:new_user_id]) + unless current_user.linked_with?(new_user) + handle_non_epp_errors(new_user, 'Cannot switch to unlinked user') + return + end + + @current_user = new_user + data = auth_values_to_data(registrar: current_user.registrar) + message = I18n.t('registrar.current_user.switch.switched', new_user: new_user) + token = Base64.urlsafe_encode64("#{new_user.username}:#{new_user.plain_text_password}") + render_success(data: { token: token, registrar: data }, message: message) + end + + api :get, '/repp/v1/accounts/balance' desc "Get account's balance" def balance resp = { balance: current_user.registrar.cash_account.balance, currency: current_user.registrar.cash_account.currency } - resp[:transactions] = activities if params[:detailed] == 'true' + if params[:detailed] == 'true' + activities = current_user.registrar.cash_account.activities.order(created_at: :desc) + activities = activities.where('created_at >= ?', params[:from]) if params[:from] + activities = activities.where('created_at <= ?', params[:until]) if params[:until] + resp[:transactions] = serialized_activities(activities) + end render_success(data: resp) end - def activities + private + + def account_params + params.require(:account).permit(:billing_email, :iban, :new_user_id) + end + + def index_params + params.permit(:id, :limit, :offset, :q, + :page, :per_page, + q: [:description_matches, :created_at_gteq, + :created_at_lteq, :s, { s: [] }, { activity_type_in: [] }]) + end + + def type_params + permitted_params = params.require(:type).permit(:amount, :threshold) + normalize_params(permitted_params) + end + + def normalize_params(params) + params[:amount] = params[:amount].to_f + params[:threshold] = params[:threshold].to_f + params + end + + def search_params + index_params.fetch(:q, {}) || {} + end + + def limit + index_params[:limit] + end + + def offset + index_params[:offset] || 0 + end + + def serialized_users(users) arr = [] - registrar_activities.each do |a| - arr << { created_at: a.created_at, description: a.description, - type: a.activity_type == 'add_credit' ? 'credit' : 'debit', - sum: a.sum, balance: a.new_balance } + users.each do |u| + arr << { id: u.id, username: u.username, + role: u.roles.first, registrar_name: u.registrar.name } end arr end - def registrar_activities - activities = current_user.registrar.cash_account.activities.order(created_at: :desc) - activities = activities.where('created_at >= ?', params[:from]) if params[:from] - activities = activities.where('created_at <= ?', params[:until]) if params[:until] + def serialized_activities(activities) + arr = [] + activities.each do |a| + arr << { created_at: a.created_at, description: a.description, + type: a.activity_type == 'add_credit' ? 'credit' : 'debit', + sum: a.sum, balance: a.new_balance, currency: a.currency, + updator: a.updator_str } + end - activities + arr end end end diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb index 3e9ab5715..d84c8e37b 100644 --- a/app/controllers/repp/v1/base_controller.rb +++ b/app/controllers/repp/v1/base_controller.rb @@ -1,12 +1,13 @@ module Repp module V1 class BaseController < ActionController::API # rubocop:disable Metrics/ClassLength + attr_reader :current_user + around_action :log_request before_action :authenticate_user before_action :validate_webclient_ca + before_action :validate_client_certs before_action :check_ip_restriction - attr_reader :current_user - before_action :set_paper_trail_whodunnit private @@ -22,6 +23,10 @@ module Repp rescue Apipie::ParamInvalid => e @response = { code: 2005, message: e.message.gsub(/\n/, '. ') } render(json: @response, status: :bad_request) + rescue CanCan::AccessDenied => e + @response = { code: 2201, message: 'Authorization error' } + logger.error e.to_s + render(json: @response, status: :unauthorized) ensure create_repp_log end @@ -65,7 +70,6 @@ module Repp def handle_errors(obj = nil) @epp_errors ||= ActiveModel::Errors.new(self) - if obj obj.construct_epp_errors obj.errors.each { |error| @epp_errors.import error } @@ -85,6 +89,12 @@ module Repp render(json: @response, status: status) end + def handle_non_epp_errors(obj, message = nil) + @response = { message: message || obj.errors.full_messages.join(', '), + data: {} } + render(json: @response, status: :bad_request) + end + def basic_token pattern = /^Basic / header = request.headers['Authorization'] @@ -95,12 +105,14 @@ module Repp def authenticate_user username, password = Base64.urlsafe_decode64(basic_token).split(':') @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) + user_active = @current_user.active? - return if @current_user + return if @current_user && user_active raise(ArgumentError) rescue NoMethodError, ArgumentError - @response = { code: 2202, message: 'Invalid authorization information' } + @response = { code: 2202, message: 'Invalid authorization information', + data: { username: username, password: password, active: user_active } } render(json: @response, status: :unauthorized) end @@ -114,7 +126,7 @@ module Repp end def webclient_request? - return if Rails.env.test? + return false if Rails.env.test? || Rails.env.development? ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) end @@ -123,6 +135,7 @@ module Repp return unless webclient_request? request_name = request.env['HTTP_SSL_CLIENT_S_DN_CN'] + webclient_cn = ENV['webclient_cert_common_name'] || 'webclient' return if request_name == webclient_cn @@ -132,9 +145,28 @@ module Repp render(json: @response, status: :unauthorized) end + def validate_client_certs + return if Rails.env.development? || Rails.env.test? + return if webclient_request? + return if @current_user.pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], + request.env['HTTP_SSL_CLIENT_S_DN_CN']) + + @response = { code: 2202, message: 'Invalid certificate' } + render(json: @response, status: :unauthorized) + end + def logger Rails.logger end + + def auth_values_to_data(registrar:) + data = current_user.as_json(only: %i[id username roles]) + data[:registrar_name] = registrar.name + data[:legaldoc_mandatory] = registrar.legaldoc_mandatory? + data[:address_processing] = Contact.address_processing? + data[:abilities] = Ability.new(current_user).permissions + data + end end end end diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb index c19ca3967..5d8f20ee0 100644 --- a/app/controllers/repp/v1/contacts_controller.rb +++ b/app/controllers/repp/v1/contacts_controller.rb @@ -3,30 +3,67 @@ module Repp module V1 class ContactsController < BaseController # rubocop:disable Metrics/ClassLength before_action :find_contact, only: %i[show update destroy] + skip_around_action :log_request, only: :search api :get, '/repp/v1/contacts' desc 'Get all existing contacts' def index - record_count = current_user.registrar.contacts.count - contacts = showable_contacts(params[:details], params[:limit] || 200, - params[:offset] || 0) - @response = { contacts: contacts, total_number_of_records: record_count } - render(json: @response, status: :ok) + authorize! :check, Epp::Contact + records = current_user.registrar.contacts + + q = records.ransack(PartialSearchFormatter.format(search_params)) + q.sorts = 'created_at desc' if q.sorts.empty? + contacts = q.result(distinct: true) + + limited_contacts = contacts.limit(limit).offset(offset) + .includes(:domain_contacts, :registrant_domains, :registrar) + + render_success(data: { contacts: serialized_contacts(limited_contacts), + count: contacts.count, statuses: Contact::STATUSES, + ident_types: Contact::Ident.types }) end + # rubocop:disable Metrics/MethodLength + api :get, '/repp/v1/contacts/search(/:id)' + desc 'Search all existing contacts by optional id or query param' + def search + scope = current_user.registrar.contacts + if params[:query] + escaped_str = ActiveRecord::Base.connection.quote_string params[:query] + scope = scope.where("name ilike '%#{escaped_str}%' OR code ilike '%#{escaped_str}%' + OR ident ilike '%#{escaped_str}%'") + elsif params[:id] + scope = scope.where(code: params[:id]) + end + + render_success(data: scope.limit(10) + .map do |c| + { value: c.code, + label: "#{c.code} #{c.name}", + selected: scope.size == 1 } + end) + end + # rubocop:enable Metrics/MethodLength + api :get, '/repp/v1/contacts/:contact_code' desc 'Get a specific contact' def show - serializer = ::Serializers::Repp::Contact.new(@contact, - show_address: Contact.address_processing?) - render_success(data: serializer.to_json) + authorize! :check, Epp::Contact + + simple = params[:simple] == 'true' || false + serializer = Serializers::Repp::Contact.new(@contact, + show_address: Contact.address_processing?, + domain_params: domain_filter_params, + simplify: simple) + + render_success(data: { contact: serializer.to_json }) end api :get, '/repp/v1/contacts/check/:contact_code' desc 'Check contact code availability' def check contact = Epp::Contact.find_by(code: params[:id]) - data = { contact: { id: params[:id], available: contact.nil? } } + data = { contact: { code: params[:id], available: contact.nil? } } render_success(data: data) end @@ -35,7 +72,7 @@ module Repp desc 'Create a new contact' def create @contact = Epp::Contact.new(contact_params_with_address, current_user.registrar, epp: false) - action = Actions::ContactCreate.new(@contact, params[:legal_document], + action = Actions::ContactCreate.new(@contact, contact_params[:legal_document], contact_ident_params) unless action.call @@ -50,7 +87,7 @@ module Repp desc 'Update existing contact' def update action = Actions::ContactUpdate.new(@contact, contact_params_with_address(required: false), - params[:legal_document], + contact_params[:legal_document], contact_ident_params(required: false), current_user) unless action.call @@ -73,29 +110,71 @@ module Repp render_success end - def contact_addr_present? - return false unless contact_addr_params.key?(:addr) + private - contact_addr_params[:addr].keys.any? + def index_params + params.permit(:id, :limit, :offset, :details, :q, :simple, + :page, :per_page, :domain_filter, + domain_filter: [], + q: %i[s name_matches code_eq ident_matches ident_type_eq + email_matches country_code_eq types_contains_array + updated_at_gteq created_at_gteq created_at_lteq + statuses_contains_array] + [s: []]) + end + + def search_params + index_params.fetch(:q, {}) || {} + end + + def domain_filter_params + filter_params = index_params.slice(:id, :page, :per_page, :domain_filter).to_h + filter_params.merge!({ sort: hashify(index_params[:q].fetch(:s)) }) if index_params[:q] + filter_params + end + + def hashify(sort) + return unless sort + + sort_hash = {} + if sort.is_a?(Array) + sort.each do |s| + sort_hash.merge!(Hash[*s.split(' ')]) + end + else + sort_hash.merge!(Hash[*sort.split(' ')]) + end + sort_hash + end + + def limit + index_params[:limit] + end + + def offset + index_params[:offset] || 0 + end + + def serialized_contacts(contacts) + return contacts.map(&:code) unless index_params[:details] == 'true' + + address_processing = Contact.address_processing? + contacts.map do |c| + Serializers::Repp::Contact.new(c, show_address: address_processing).to_json + end + end + + def contact_addr_present? + return false unless contact_addr_params + + contact_addr_params.keys.any? end def create_update_success_body - { code: opt_addr? ? 1100 : nil, data: { contact: { id: @contact.code } }, + { code: opt_addr? ? 1100 : nil, + data: { contact: { code: @contact.code } }, message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : nil } end - def showable_contacts(details, limit, offset) - contacts = current_user.registrar.contacts.limit(limit).offset(offset) - - return contacts.pluck(:code) unless details - - contacts.map do |contact| - serializer = ::Serializers::Repp::Contact.new(contact, - show_address: Contact.address_processing?) - serializer.to_json - end - end - def opt_addr? !Contact.address_processing? && contact_addr_present? end @@ -106,36 +185,36 @@ module Repp end def contact_params_with_address(required: true) - return contact_create_params(required: required) unless contact_addr_params.key?(:addr) + return contact_create_params(required: required) unless contact_addr_present? - addr = {} - contact_addr_params[:addr].each_key { |k| addr[k] = contact_addr_params[:addr][k] } - contact_create_params(required: required).merge(addr) + contact_create_params(required: required).merge(contact_addr_params) end def contact_create_params(required: true) - params.require(:contact).require(%i[name email phone]) if required - params.require(:contact).permit(:name, :email, :phone, :id) + create_params = %i[name email phone] + contact_params.require(create_params) if required + contact_params.slice(:id, *create_params) end def contact_ident_params(required: true) - if required - params.require(:contact).require(:ident).require(%i[ident ident_type ident_country_code]) - params.require(:contact).require(:ident).permit(:ident, :ident_type, :ident_country_code) - else - params.permit(contact: { ident: %i[ident ident_type ident_country_code] }) - end - - params[:contact][:ident] + ident_params = %i[ident ident_type ident_country_code] + contact_params.require(:ident).require(ident_params) if required + contact_params[:ident].to_h end def contact_addr_params - if Contact.address_processing? - params.require(:contact).require(:addr).require(%i[country_code city street zip]) - params.require(:contact).require(:addr).permit(:country_code, :city, :street, :zip) - else - params.require(:contact).permit(addr: %i[country_code city street zip]) - end + return contact_params[:addr] unless Contact.address_processing? + + addr_params = %i[country_code city street zip] + contact_params.require(:addr).require(addr_params) + contact_params[:addr] + end + + def contact_params + params.require(:contact).permit(:id, :name, :email, :phone, :legal_document, + legal_document: %i[body type], + ident: [%i[ident ident_type ident_country_code]], + addr: [%i[country_code city street zip state]]) end end end diff --git a/app/controllers/repp/v1/domains/admin_contacts_controller.rb b/app/controllers/repp/v1/domains/admin_contacts_controller.rb index 6ec0e129b..5db865199 100644 --- a/app/controllers/repp/v1/domains/admin_contacts_controller.rb +++ b/app/controllers/repp/v1/domains/admin_contacts_controller.rb @@ -7,7 +7,7 @@ module Repp unless @new_contact.identical_to?(@current_contact) @epp_errors.add(:epp_errors, - msg: 'Admin contacts must be identical', + msg: 'New and current admin contacts ident data must be identical', code: '2304') end diff --git a/app/controllers/repp/v1/domains/base_contacts_controller.rb b/app/controllers/repp/v1/domains/base_contacts_controller.rb index 65dbea9ac..225b14b58 100644 --- a/app/controllers/repp/v1/domains/base_contacts_controller.rb +++ b/app/controllers/repp/v1/domains/base_contacts_controller.rb @@ -2,19 +2,16 @@ module Repp module V1 module Domains class BaseContactsController < BaseController - before_action :set_current_contact, only: [:update] - before_action :set_new_contact, only: [:update] + before_action :set_contacts, only: [:update] - def set_current_contact - @current_contact = current_user.registrar.contacts - .find_by!(code: contact_params[:current_contact_id]) - end - - def set_new_contact - @new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id]) + def set_contacts + contacts = current_user.registrar.contacts + @current_contact = contacts.find_by!(code: contact_params[:current_contact_id]) + @new_contact = contacts.find_by!(code: contact_params[:new_contact_id]) end def update + authorize! :manage, :repp @epp_errors ||= ActiveModel::Errors.new(self) return unless @new_contact.invalid? @@ -26,8 +23,11 @@ module Repp private def contact_params - params.require(%i[current_contact_id new_contact_id]) - params.permit(:current_contact_id, :new_contact_id) + param_list = %i[current_contact_id new_contact_id] + params.require(param_list) + params.permit(:current_contact_id, :new_contact_id, + contact: {}, + admin_contact: [param_list]) end end end diff --git a/app/controllers/repp/v1/domains/contacts_controller.rb b/app/controllers/repp/v1/domains/contacts_controller.rb index 4c89243c7..b41b3a378 100644 --- a/app/controllers/repp/v1/domains/contacts_controller.rb +++ b/app/controllers/repp/v1/domains/contacts_controller.rb @@ -14,8 +14,8 @@ module Repp api :GET, '/repp/v1/domains/:domain_name/contacts' desc "View domain's admin and tech contacts" def index - admin_contacts = @domain.admin_domain_contacts.pluck(:contact_code_cache) - tech_contacts = @domain.tech_domain_contacts.pluck(:contact_code_cache) + admin_contacts = @domain.admin_domain_contacts.map(&:contact).pluck(:code) + tech_contacts = @domain.tech_domain_contacts.map(&:contact).pluck(:code) data = { admin_contacts: admin_contacts, tech_contacts: tech_contacts } render_success(data: data) @@ -38,7 +38,6 @@ module Repp def cta(action = 'add') params[:contacts].each { |c| c[:action] = action } action = Actions::DomainUpdate.new(@domain, contact_create_params, false) - # rubocop:disable Style/AndOr handle_errors(@domain) and return unless action.call # rubocop:enable Style/AndOr diff --git a/app/controllers/repp/v1/domains/renews_controller.rb b/app/controllers/repp/v1/domains/renews_controller.rb index af40e17b1..9aba3e41b 100644 --- a/app/controllers/repp/v1/domains/renews_controller.rb +++ b/app/controllers/repp/v1/domains/renews_controller.rb @@ -8,14 +8,14 @@ module Repp api :POST, 'repp/v1/domains/:domain_name/renew' desc 'Renew domain' - param :renew, Hash, required: true, desc: 'Renew parameters' do + param :renews, Hash, required: true, desc: 'Renew parameters' do param :period, Integer, required: true, desc: 'Renew period. Month (m) or year (y)' param :period_unit, String, required: true, desc: 'For how many months or years to renew' param :exp_date, String, required: true, desc: 'Current expiry date for domain' end def create authorize!(:renew, @domain) - action = Actions::DomainRenew.new(@domain, renew_params[:renew], current_user.registrar) + action = Actions::DomainRenew.new(@domain, renew_params[:renews], current_user.registrar) unless action.call handle_errors(@domain) @@ -26,10 +26,11 @@ module Repp end def bulk_renew + authorize! :manage, :repp renew = run_bulk_renew_task(@domains, bulk_renew_params[:renew_period]) return render_success(data: { updated_domains: @domains.map(&:name) }) if renew.valid? - msg = renew.errors.keys.map { |k, _v| renew.errors[k] }.join(', ') + msg = renew.errors.attribute_names.map { |k, _v| renew.errors[k] }.join(', ') @epp_errors.add(:epp_errors, msg: msg, code: '2002') handle_errors end @@ -37,7 +38,7 @@ module Repp private def renew_params - params.permit(:domain_id, renew: %i[period period_unit exp_date]) + params.permit(:domain_id, renews: %i[period period_unit exp_date]) end def validate_renew_period @@ -50,13 +51,11 @@ module Repp def select_renewable_domains @epp_errors ||= ActiveModel::Errors.new(self) - - if bulk_renew_params[:domains].instance_of?(Array) - @domains = bulk_renew_domains - else - @epp_errors.add(:epp_errors, msg: 'Domains attribute must be an array', code: '2005') + @domains = bulk_renew_domains + if @domains.empty? + @epp_errors.add(:epp_errors, msg: 'Domains cannot be empty', + code: '2005') end - return handle_errors if @epp_errors.any? end @@ -75,14 +74,18 @@ module Repp def bulk_renew_domains @epp_errors ||= ActiveModel::Errors.new(self) domains = [] - bulk_renew_params[:domains].each do |idn| - domain = Epp::Domain.find_by(name: idn) - domains << domain if domain - next if domain + if bulk_renew_params[:domains].instance_of?(Array) + bulk_renew_params[:domains].each do |idn| + domain = Epp::Domain.find_by(name: idn) + domains << domain if domain + next if domain - @epp_errors.add(:epp_errors, - msg: "Object does not exist: #{idn}", - code: '2304') + @epp_errors.add(:epp_errors, + msg: "Object does not exist: #{idn}", + code: '2304') + end + else + @epp_errors.add(:epp_errors, msg: 'Domains attribute must be an array', code: '2005') end domains diff --git a/app/controllers/repp/v1/domains/statuses_controller.rb b/app/controllers/repp/v1/domains/statuses_controller.rb index ee15655df..d46725c46 100644 --- a/app/controllers/repp/v1/domains/statuses_controller.rb +++ b/app/controllers/repp/v1/domains/statuses_controller.rb @@ -7,7 +7,6 @@ module Repp api :DELETE, '/repp/v1/domains/:domain_name/statuses/:status' param :domain_name, String, desc: 'Domain name' - param :status, String, desc: 'Status to be removed' desc 'Remove status from specific domain' def destroy return editing_failed unless domain_with_status?(params[:id]) @@ -22,7 +21,6 @@ module Repp api :PUT, '/repp/v1/domains/:domain_name/statuses/:status' param :domain_name, String, desc: 'Domain name' - param :status, String, desc: 'Status to be added' desc 'Add status to specific domain' def update return editing_failed if domain_with_status?(params[:id]) diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index 06d4a0330..6990b0a86 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -3,6 +3,7 @@ module Repp module V1 class DomainsController < BaseController # rubocop:disable Metrics/ClassLength before_action :set_authorized_domain, only: %i[transfer_info destroy] + before_action :find_password, only: %i[update destroy] before_action :validate_registrar_authorization, only: %i[transfer_info destroy] before_action :forward_registrar_id, only: %i[create update destroy] before_action :set_domain, only: %i[update] @@ -10,20 +11,31 @@ module Repp api :GET, '/repp/v1/domains' desc 'Get all existing domains' def index - records = current_user.registrar.domains - domains = records.limit(limit).offset(offset) + authorize! :info, Epp::Domain + records = current_user.registrar.domains.includes(:registrar, :registrant) + q = records.ransack(PartialSearchFormatter.format(search_params)) + q.sorts = ['valid_to asc', 'created_at desc'] if q.sorts.empty? + # use distinct: false here due to ransack bug: + # https://github.com/activerecord-hackery/ransack/issues/429 + domains = q.result(distinct: false) - render_success(data: { domains: serialized_domains(domains), - total_number_of_records: records.count }) + limited_domains = domains.limit(limit).offset(offset) + + render_success(data: { new_domain: records.any? ? serialized_domains([records.last]) : [], + domains: serialized_domains(limited_domains.to_a.uniq), + count: domains.count, + statuses: DomainStatus::STATUSES }) end api :GET, '/repp/v1/domains/:domain_name' desc 'Get a specific domain' def show - @domain = Epp::Domain.find_by!(name: params[:id]) + @domain = Epp::Domain.find_by(name: params[:id]) + authorize! :info, @domain + sponsor = @domain.registrar == current_user.registrar - render_success(data: { domain: Serializers::Repp::Domain.new(@domain, - sponsored: sponsor).to_json }) + serializer = Serializers::Repp::Domain.new(@domain, sponsored: sponsor) + render_success(data: { domain: serializer.to_json }) end api :POST, '/repp/v1/domains' @@ -33,7 +45,7 @@ module Repp param :registrant, String, required: true, desc: 'Registrant contact code' param :reserved_pw, String, required: false, desc: 'Reserved password for domain' param :transfer_code, String, required: false, desc: 'Desired transfer code for domain' - # param :period, String, required: true, desc: 'Registration period in months or years' + param :period, Integer, required: true, desc: 'Registration period in months or years' param :period_unit, String, required: true, desc: 'Period type (month m) or (year y)' param :nameservers_attributes, Array, required: false, desc: 'Domain nameservers' do param :hostname, String, required: true, desc: 'Nameserver hostname' @@ -56,15 +68,18 @@ module Repp end end def create - authorize!(:create, Epp::Domain) + authorize! :create, Epp::Domain @domain = Epp::Domain.new - action = Actions::DomainCreate.new(@domain, domain_create_params) + + action = Actions::DomainCreate.new(@domain, domain_params) # rubocop:disable Style/AndOr handle_errors(@domain) and return unless action.call # rubocop:enable Style/AndOr - render_success(data: { domain: { name: @domain.name, transfer_code: @domain.transfer_code } }) + render_success(data: { domain: { name: @domain.name, + transfer_code: @domain.transfer_code, + id: @domain.reload.uuid } }) end api :PUT, '/repp/v1/domains/:domain_name' @@ -73,14 +88,14 @@ module Repp param :domain, Hash, required: true, desc: 'Changes of domain object' do param :registrant, Hash, required: false, desc: 'New registrant object' do param :code, String, required: true, desc: 'New registrant contact code' - param :verified, [true, false], required: false, - desc: 'Registrant change is already verified' + param :verified, [true, false, 'true', 'false'], required: false, + desc: 'Registrant change is already verified' end param :transfer_code, String, required: false, desc: 'New authorization code' end def update - action = Actions::DomainUpdate.new(@domain, params[:domain], false) - + authorize!(:update, @domain, @password) + action = Actions::DomainUpdate.new(@domain, update_params, false) unless action.call handle_errors(@domain) return @@ -108,23 +123,28 @@ module Repp api :POST, '/repp/v1/domains/transfer' desc 'Transfer multiple domains' def transfer + authorize! :transfer, Epp::Domain @errors ||= [] @successful = [] - transfer_params[:domain_transfers].each do |transfer| initiate_transfer(transfer) end + render_success(data: { success: @successful, failed: @errors }) end api :DELETE, '/repp/v1/domains/:domain_name' desc 'Delete specific domain' - param :delete, Hash, required: true, desc: 'Object holding verified key' do - param :verified, [true, false], required: true, - desc: 'Whether to ask registrant verification or not' + param :id, String, desc: 'Domain name in IDN / Puny format' + param :domain, Hash, required: true, desc: 'Changes of domain object' do + param :delete, Hash, required: true, desc: 'Object holding verified key' do + param :verified, [true, false, 'true', 'false'], required: true, + desc: 'Whether to ask registrant verification or not' + end end def destroy - action = Actions::DomainDelete.new(@domain, params, current_user.registrar) + authorize!(:delete, @domain, @password) + action = Actions::DomainDelete.new(@domain, domain_params, current_user.registrar) # rubocop:disable Style/AndOr handle_errors(@domain) and return unless action.call @@ -138,7 +158,8 @@ module Repp def serialized_domains(domains) return domains.pluck(:name) unless index_params[:details] == 'true' - domains.map { |d| Serializers::Repp::Domain.new(d).to_json } + simple = index_params[:simple] == 'true' || false + domains.map { |d| Serializers::Repp::Domain.new(d, simplify: simple).to_json } end def initiate_transfer(transfer) @@ -155,18 +176,13 @@ module Repp end def transfer_params - params.require(:data).require(:domain_transfers).each do |t| - t.require(:domain_name) - t.permit(:domain_name) - t.require(:transfer_code) - t.permit(:transfer_code) - end - params.require(:data).permit(domain_transfers: %i[domain_name transfer_code]) + params.require(:data).require(:domain_transfers) + params.require(:data).permit(domain_transfers: [%i[domain_name transfer_code]]) end def transfer_info_params params.require(:id) - params.permit(:id) + params.permit(:id, :legal_document, delete: [:verified]) end def forward_registrar_id @@ -177,6 +193,7 @@ module Repp def set_domain registrar = current_user.registrar + @domain = Epp::Domain.find_by(registrar: registrar, name: params[:id]) @domain ||= Epp::Domain.find_by!(registrar: registrar, name_puny: params[:id]) @@ -185,6 +202,10 @@ module Repp raise ActiveRecord::RecordNotFound end + def find_password + @password = domain_params[:transfer_code] + end + def set_authorized_domain @epp_errors ||= ActiveModel::Errors.new(self) @domain = domain_from_url_hash @@ -201,14 +222,14 @@ module Repp end def domain_from_url_hash - entry = transfer_info_params[:id] + entry = params[:id] return Epp::Domain.find(entry) if entry.match?(/\A[0-9]+\z/) Epp::Domain.find_by!('name = ? OR name_puny = ?', entry, entry) end def limit - index_params[:limit] || 200 + index_params[:limit] end def offset @@ -216,15 +237,47 @@ module Repp end def index_params - params.permit(:limit, :offset, :details) + params.permit(:limit, :offset, :details, :simple, :q, + q: %i[s name_matches registrant_code_eq contacts_ident_eq + nameservers_hostname_eq valid_to_gteq valid_to_lteq + statuses_contains_array] + [s: []]) end - def domain_create_params - params.require(:domain).permit(:name, :registrant, :period, :period_unit, :registrar, - :transfer_code, :reserved_pw, - dnskeys_attributes: [%i[flags alg protocol public_key]], + def search_params + index_params.fetch(:q, {}) || {} + end + + def update_params + dup_params = domain_params.to_h.dup + return dup_params unless dup_params[:contacts] + + modify_contact_params(dup_params) + end + + def modify_contact_params(params) + new_contact_params = params[:contacts].map { |c| c.to_h.symbolize_keys } + old_contact_params = @domain.domain_contacts.includes(:contact).map do |c| + { code: c.contact.code, type: c.name.downcase } + end + params[:contacts] = (new_contact_params - old_contact_params).map do |c| + c.merge(action: 'add') + end + params[:contacts].concat((old_contact_params - new_contact_params) + .map { |c| c.merge(action: 'rem') }) + params + end + + def domain_params + params.require(:domain).permit(:name, :period, :period_unit, :registrar, :transfer_code, + :reserved_pw, :legal_document, :registrant, + legal_document: %i[body type], registrant: [%i[code verified]], + dns_keys: [%i[id flags alg protocol public_key action]], + nameservers: [[:id, :hostname, :action, { ipv4: [], ipv6: [] }]], + contacts: [%i[code type action]], nameservers_attributes: [[:hostname, { ipv4: [], ipv6: [] }]], - admin_contacts: [], tech_contacts: []) + admin_contacts: [], tech_contacts: [], + dnskeys_attributes: [%i[flags alg protocol public_key]], + delete: [:verified]) end end end diff --git a/app/controllers/repp/v1/invoices_controller.rb b/app/controllers/repp/v1/invoices_controller.rb new file mode 100644 index 000000000..2d4340b3e --- /dev/null +++ b/app/controllers/repp/v1/invoices_controller.rb @@ -0,0 +1,128 @@ +require 'serializers/repp/invoice' +module Repp + module V1 + class InvoicesController < BaseController # rubocop:disable Metrics/ClassLength + load_and_authorize_resource + + # rubocop:disable Metrics/MethodLength + api :get, '/repp/v1/invoices' + desc 'Get all invoices' + def index + records = current_user.registrar.invoices + q = records.ransack(PartialSearchFormatter.format(search_params)) + q.sorts = 'created_at desc' if q.sorts.empty? + invoices = q.result(distinct: true) + + limited_invoices = invoices.limit(limit).offset(offset) + .includes(:items, :account_activity, :buyer) + + render_success(data: { invoices: serialized_invoices(limited_invoices), + count: invoices.count }) + end + # rubocop:enable Metrics/MethodLength + + api :get, '/repp/v1/invoices/:id' + desc 'Get a specific invoice' + def show + serializer = Serializers::Repp::Invoice.new(@invoice) + render_success(data: { invoice: serializer.to_json }) + end + + api :get, '/repp/v1/invoices/:id/download' + desc 'Download a specific invoice as pdf file' + def download + filename = "Invoice-#{@invoice.number}.pdf" + @response = { code: 1000, message: 'Command completed successfully', + data: filename } + send_data @invoice.as_pdf, filename: filename + end + + api :post, '/repp/v1/invoices/:id/send_to_recipient' + desc 'Send invoice pdf to recipient' + param :invoice, Hash, required: true, desc: 'Invoice data for sending to recipient' do + param :id, String, required: true, desc: 'Invoice id' + param :recipient, String, required: true, desc: 'Invoice receipient email' + end + def send_to_recipient + recipient = invoice_params[:recipient] + if recipient.blank? + handle_non_epp_errors(@invoice, 'Invoice recipient cannot be empty') + return + end + + InvoiceMailer.invoice_email(invoice: @invoice, recipient: recipient) + .deliver_now + serializer = Serializers::Repp::Invoice.new(@invoice, simplify: true) + render_success(data: { invoice: serializer.to_json + .merge!(recipient: recipient) }) + end + + api :put, '/repp/v1/invoices/:id/cancel' + desc 'Cancel a specific invoice' + def cancel + action = Actions::InvoiceCancel.new(@invoice) + if action.call + EisBilling::SendInvoiceStatus.send_info(invoice_number: @invoice.number, + status: 'cancelled') + else + handle_non_epp_errors(@invoice) + return + end + + serializer = Serializers::Repp::Invoice.new(@invoice, simplify: true) + render_success(data: { invoice: serializer.to_json }) + end + + api :post, '/repp/v1/invoices/add_credit' + desc 'Generate add credit invoice' + def add_credit + deposit = Deposit.new(invoice_params.merge(registrar: current_user.registrar)) + invoice = deposit.issue_prepayment_invoice + if invoice + serializer = Serializers::Repp::Invoice.new(invoice, simplify: true) + render_success(data: { invoice: serializer.to_json }) + else + handle_non_epp_errors(deposit) + end + end + + private + + def index_params + params.permit(:id, :limit, :offset, :details, :q, :simple, + :page, :per_page, + q: %i[number_str_matches due_date_gteq due_date_lteq + account_activity_created_at_gteq + account_activity_created_at_lteq + account_activity_id_not_null + account_activity_id_null cancelled_at_null + cancelled_at_not_null number_gteq number_lteq + monthly_invoice_true monthly_invoice_false + total_gteq total_lteq s] + [s: []]) + end + + def search_params + index_params.fetch(:q, {}) || {} + end + + def invoice_params + params.require(:invoice).permit(:id, :recipient, :amount, :description) + end + + def limit + index_params[:limit] || 200 + end + + def offset + index_params[:offset] || 0 + end + + def serialized_invoices(invoices) + return invoices.map(&:number) unless index_params[:details] == 'true' + + simple = index_params[:simple] == 'true' || false + invoices.map { |i| Serializers::Repp::Invoice.new(i, simplify: simple).to_json } + end + end + end +end diff --git a/app/controllers/repp/v1/registrar/auth_controller.rb b/app/controllers/repp/v1/registrar/auth_controller.rb new file mode 100644 index 000000000..46c21459e --- /dev/null +++ b/app/controllers/repp/v1/registrar/auth_controller.rb @@ -0,0 +1,35 @@ +module Repp + module V1 + module Registrar + class AuthController < BaseController + skip_before_action :authenticate_user, only: :tara_callback + skip_before_action :check_ip_restriction, only: :tara_callback + skip_before_action :validate_client_certs, only: :tara_callback + + api :GET, 'repp/v1/registrar/auth' + desc 'check user auth info and return data' + def index + registrar = current_user.registrar + render_success(data: auth_values_to_data(registrar: registrar)) + end + + api :POST, 'repp/v1/registrar/auth/tara_callback' + desc 'check tara callback omniauth user info and return token' + def tara_callback + user = ApiUser.from_omniauth(auth_params) + response = { code: 401, message: I18n.t(:no_such_user), data: {} } + render(json: response, status: :unauthorized) and return unless user && user&.active + + token = Base64.urlsafe_encode64("#{user.username}:#{user.plain_text_password}") + render_success(data: { token: token, username: user.username }) + end + + private + + def auth_params + params.require(:auth).permit(:uid, :new_user_id) + end + end + end + end +end diff --git a/app/controllers/repp/v1/registrar/nameservers_controller.rb b/app/controllers/repp/v1/registrar/nameservers_controller.rb index 174193350..b3c6d8412 100644 --- a/app/controllers/repp/v1/registrar/nameservers_controller.rb +++ b/app/controllers/repp/v1/registrar/nameservers_controller.rb @@ -19,15 +19,17 @@ module Repp end def update # rubocop:disable Metrics/MethodLength + authorize! :manage, :repp affected, errored = if hostname.present? - current_user.registrar.replace_nameservers(hostname, - hostname_params[:data][:attributes], - domains: domains_from_params) + current_user.registrar + .replace_nameservers(hostname, + hostname_params[:attributes], + domains: domains_from_params) else - current_user.registrar.add_nameservers(hostname_params[:data][:attributes], - domains: domains_from_params) + current_user.registrar + .add_nameservers(hostname_params[:attributes], + domains: domains_from_params) end - render_success(data: data_format_for_success(affected, errored)) rescue ActiveRecord::RecordInvalid => e handle_errors(e.record) @@ -36,34 +38,33 @@ module Repp private def domains_from_params - return [] unless params[:data][:domains] + return [] unless hostname_params[:domains] - params[:data][:domains].map(&:downcase) + hostname_params[:domains].map(&:downcase) end def data_format_for_success(affected_domains, errored_domains) { type: 'nameserver', - id: params[:data][:attributes][:hostname], - attributes: params[:data][:attributes], - affected_domains: affected_domains, - skipped_domains: errored_domains, + id: hostname_params[:attributes][:hostname], + attributes: hostname_params[:attributes], + affected_domains: affected_domains || [], + skipped_domains: errored_domains || [], } end def hostname_params - params.require(:data).require(%i[type]) - params.require(:data).require(:attributes).require([:hostname]) - - params.permit(data: [ - :type, :id, - { domains: [], - attributes: [:hostname, { ipv4: [], ipv6: [] }] } - ]) + params.require(:data).permit(:type, :id, + :domains, nameserver: [], domains: [], + attributes: [:hostname, { ipv4: [], ipv6: [] }]) + .tap do |data| + data.require(:type) + data.require(:attributes).require([:hostname]) + end end def hostname - hostname_params[:data][:id] || nil + hostname_params[:id] || nil end def verify_nameserver_existance diff --git a/app/controllers/repp/v1/registrar/notifications_controller.rb b/app/controllers/repp/v1/registrar/notifications_controller.rb index 815ee85b9..6b1d342cc 100644 --- a/app/controllers/repp/v1/registrar/notifications_controller.rb +++ b/app/controllers/repp/v1/registrar/notifications_controller.rb @@ -2,7 +2,7 @@ module Repp module V1 module Registrar class NotificationsController < BaseController - before_action :set_notification, only: [:update] + before_action :set_notification, only: %i[update show] api :GET, '/repp/v1/registrar/notifications' desc 'Get the latest unread poll message' @@ -39,7 +39,6 @@ module Repp api :GET, '/repp/v1/registrar/notifications/:notification_id' desc 'Get a specific poll message' def show - @notification = current_user.registrar.notifications.find(params[:id]) data = @notification.as_json(only: %i[id text attached_obj_id attached_obj_type read]) render_success(data: data) @@ -51,6 +50,7 @@ module Repp param :read, [true, 'true'], required: true, desc: 'Set as true to mark as read' end def update + authorize! :manage, :poll # rubocop:disable Style/AndOr handle_errors(@notification) and return unless @notification.mark_as_read # rubocop:enable Style/AndOr diff --git a/app/controllers/repp/v1/registrar/summary_controller.rb b/app/controllers/repp/v1/registrar/summary_controller.rb new file mode 100644 index 000000000..a0e266e93 --- /dev/null +++ b/app/controllers/repp/v1/registrar/summary_controller.rb @@ -0,0 +1,114 @@ +module Repp + module V1 + module Registrar + class SummaryController < BaseController + api :GET, 'repp/v1/registrar/summary' + desc 'check user summary info and return data' + + def index + user = current_user + registrar = user.registrar + if can?(:manage, :poll) + user_notifications = user.unread_notifications + notification = user_notifications.order('created_at DESC').take + end + render_success(data: serialize_data(registrar: registrar, + notification: notification, + notifications_count: user_notifications&.count, + object: notification_object(notification))) + end + + def serialized_domain_transfer(object) + { + name: object.domain_name, trStatus: object.status, + reID: object.new_registrar.code, + reDate: object.transfer_requested_at.try(:iso8601), + acID: object.old_registrar.code, + acDate: object.transferred_at.try(:iso8601) || object.wait_until.try(:iso8601), + exDate: object.domain_valid_to.iso8601 + } + end + + def serialized_contact_update_action(object) + { + contacts: object.to_non_available_contact_codes, + operation: object.operation, + opDate: object.created_at.utc.xmlschema, + svTrid: object.id, + who: object.user.username, + reason: 'Auto-update according to official data', + } + end + + private + + # rubocop:disable Style/RescueStandardError + def notification_object(notification) + return unless notification&.attached_obj_type || notification&.attached_obj_id + + object_by_type(notification.attached_obj_type).find(notification.attached_obj_id) + rescue => e + # the data model might be inconsistent; or ... + # this could happen if the registrar does not dequeue messages, + # and then the domain was deleted + # SELECT messages.id, domains.name, messages.body FROM messages LEFT OUTER + # JOIN domains ON attached_obj_id::INTEGER = domains.id + # WHERE attached_obj_type = 'Epp::Domain' AND name IS NULL; + message = 'orphan message, domain deleted, registrar should dequeue: ' + Rails.logger.error message + e.to_s + end + # rubocop:enable Style/RescueStandardError + + def object_by_type(object_type) + Object.const_get(object_type) + rescue NameError + Object.const_get("Version::#{object_type}") + end + + # rubocop:disable Metrics/MethodLength + def serialize_data(registrar:, notification:, notifications_count:, object: nil) + data = current_user.as_json(only: %i[id username]) + data[:registrar_name] = registrar.name + data[:registrar_reg_no] = registrar.reg_no + data[:balance] = { amount: registrar.cash_account&.balance, + currency: registrar.cash_account&.currency } + data[:last_login_date] = last_login_date + data[:domains] = registrar.domains.count + data[:contacts] = registrar.contacts.count + data[:phone] = registrar.phone + data[:email] = registrar.email + data[:billing_email] = registrar.billing_email + data[:billing_address] = registrar.address + data[:notification] = serialized_notification(notification, object) + data[:notifications_count] = notifications_count + data + end + # rubocop:enable Metrics/MethodLength + + def last_login_date + q = ApiLog::ReppLog.ransack({ request_path_eq: '/repp/v1/registrar/auth', + response_code_eq: '200', + api_user_name_cont: current_user.username, + request_method_eq: 'GET' }) + q.sorts = 'id desc' + q.result.offset(1).first&.created_at + end + + def serialized_notification(notification, object) + return unless notification + + notification.created_at = notification.created_at.utc.xmlschema + obj_data = serialized_object(object, notification.attached_obj_type) + notification.as_json(only: %i[id text created_at attached_obj_id attached_obj_type]) + .merge({ attached_obj_data: obj_data }) + end + + def serialized_object(object, obj_type) + return unless object + + try("serialized_#{obj_type.underscore}", object) + end + end + end + end +end diff --git a/app/controllers/repp/v1/stats_controller.rb b/app/controllers/repp/v1/stats_controller.rb new file mode 100644 index 000000000..480c81a6b --- /dev/null +++ b/app/controllers/repp/v1/stats_controller.rb @@ -0,0 +1,100 @@ +module Repp + module V1 + class StatsController < BaseController + api :get, '/repp/v1/stats/market_share_distribution' + desc 'Get market share and distribution of registrars' + param :q, Hash, required: true, desc: 'Period parameters for data' do + param :end_date, String, required: true, desc: 'Period end date' + end + def market_share_distribution + registrars = ::Registrar.where(test_registrar: false).joins(:domains) + .where(from_condition).where(to_condition) + grouped = registrars.group(:name).count + + result = grouped.map do |key, value| + hash = { name: key.strip, y: value } + hash.merge!({ sliced: true, selected: true }) if current_user.registrar.name == key + hash + end + render_success(data: result) + end + + # rubocop:disable Metrics/MethodLength + api :get, '/repp/v1/stats/market_share_growth_rate' + desc 'Get market share and growth rate of registrars' + param :q, Hash, required: true, desc: 'Period parameters for data' do + param :end_date, String, required: true, desc: 'Period end date' + param :compare_to_date, String, required: true, desc: 'Comparison date' + end + def market_share_growth_rate + registrars = ::Registrar.where(test_registrar: false).joins(:domains) + + domains_by_rar = registrars.where(from_condition).where(to_condition).group(:name).count + prev_domains_by_rar = registrars.where(compare_to_condition).group(:name).count + + set_zero_values!(domains_by_rar, prev_domains_by_rar) + + market_share_by_rar = calculate_market_share(domains_by_rar) + prev_market_share_by_rar = calculate_market_share(prev_domains_by_rar) + + result = { prev_data: { name: search_params[:compare_to_date], + domains: serialize(prev_domains_by_rar), + market_share: serialize(prev_market_share_by_rar) }, + data: { name: search_params[:end_date], + domains: serialize(domains_by_rar), + market_share: serialize(market_share_by_rar) } } + render_success(data: result) + end + # rubocop:enable Metrics/MethodLength + + private + + def search_params + params.permit(:q, q: %i[start_date end_date compare_to_date]) + .fetch(:q, {}) || {} + end + + def from_condition + return unless search_params[:start_date] + + "domains.created_at >= '#{to_date(search_params[:start_date])}'" + end + + def to_condition + return unless search_params[:end_date] + + "domains.created_at <= '#{to_date(search_params[:end_date]).end_of_month}'" + end + + def compare_to_condition + return unless search_params[:compare_to_date] + + "domains.created_at <= '#{to_date(search_params[:compare_to_date]).end_of_month}'" + end + + def to_date(date_param) + Date.strptime(date_param, '%m.%y') + end + + def serialize(rars) + rars.map { |key, value| [key, value] } + end + + def set_zero_values!(cur, prev) + cur_dup = cur.dup + cur_dup.each_key do |k| + cur_dup[k] = prev[k] || 0 + end + prev.clear.merge!(cur_dup) + end + + def calculate_market_share(domains_by_rar) + sum = domains_by_rar.values.sum + domains_by_rar.transform_values do |v| + value = v.to_f / sum * 100.0 + value < 0.1 ? value.round(3) : value.round(1) + end + end + end + end +end diff --git a/app/helpers/auction_helper.rb b/app/helpers/auction_helper.rb new file mode 100644 index 000000000..25cf463af --- /dev/null +++ b/app/helpers/auction_helper.rb @@ -0,0 +1,19 @@ +module AuctionHelper + include ActionView::Helpers::TagHelper + + def colorize_auction(auction) + case auction.status + when 'started' then render_status_black(auction.domain) + when 'awaiting_payment' then render_status_black(auction.domain) + else render_status_green(auction.domain) + end + end + + def render_status_black(name) + tag.span name.to_s, style: 'color: black;' + end + + def render_status_green(name) + tag.span name.to_s, style: 'color: green;' + end +end diff --git a/app/helpers/registrant/application_helper.rb b/app/helpers/registrant/application_helper.rb deleted file mode 100644 index 6451f91a2..000000000 --- a/app/helpers/registrant/application_helper.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Registrant::ApplicationHelper - def env_style - return '' if unstable_env.nil? - "background-image: url(#{image_path("registrar/bg-#{unstable_env}.png")});" - end -end diff --git a/app/interactions/actions/contact_create.rb b/app/interactions/actions/contact_create.rb index 84ed10caf..f3e6560b8 100644 --- a/app/interactions/actions/contact_create.rb +++ b/app/interactions/actions/contact_create.rb @@ -19,12 +19,12 @@ module Actions def maybe_change_email return if Rails.env.test? - [:regex, :mx].each do |m| + %i[regex mx].each do |m| result = Actions::SimpleMailValidator.run(email: contact.email, level: m) - next if result - contact.add_epp_error('2005', nil, "email didn't pass validation", I18n.t(:parameter_value_syntax_error)) + err_text = "email '#{contact.email}' didn't pass validation" + contact.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}") @error = true return end diff --git a/app/interactions/actions/contact_update.rb b/app/interactions/actions/contact_update.rb index 685f1474c..9092d76c9 100644 --- a/app/interactions/actions/contact_update.rb +++ b/app/interactions/actions/contact_update.rb @@ -23,11 +23,12 @@ module Actions def maybe_change_email return if Rails.env.test? - [:regex, :mx].each do |m| + %i[regex mx].each do |m| result = Actions::SimpleMailValidator.run(email: @new_attributes[:email], level: m) next if result - contact.add_epp_error('2005', nil, "email didn't pass validation", I18n.t(:parameter_value_syntax_error)) + err_text = "email '#{new_attributes[:email]}' didn't pass validation" + contact.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}") @error = true return end @@ -115,8 +116,9 @@ module Actions contact.email_history = old_email updated = contact.save - if updated && email_changed && contact.registrant? - ContactMailer.email_changed(contact: contact, old_email: old_email).deliver_now + if updated && email_changed + contact.validation_events.where('event_data @> ?', { 'email': old_email }.to_json).destroy_all + ContactMailer.email_changed(contact: contact, old_email: old_email).deliver_now if contact.registrant? end updated diff --git a/app/interactions/actions/domain_create.rb b/app/interactions/actions/domain_create.rb index 8fd25df0f..970d74b3b 100644 --- a/app/interactions/actions/domain_create.rb +++ b/app/interactions/actions/domain_create.rb @@ -120,7 +120,7 @@ module Actions contact = Contact.find_by(code: contact_code) arr = admin ? @admin_contacts : @tech_contacts if contact - arr << { contact_id: contact.id, contact_code_cache: contact.code } + arr << { contact_id: contact.id, contact_code: contact.code } else domain.add_epp_error('2303', 'contact', contact_code, %i[domain_contacts not_found]) end diff --git a/app/interactions/actions/domain_delete.rb b/app/interactions/actions/domain_delete.rb index 7790c25a9..750f0abac 100644 --- a/app/interactions/actions/domain_delete.rb +++ b/app/interactions/actions/domain_delete.rb @@ -32,7 +32,7 @@ module Actions def verify? return false unless Setting.request_confirmation_on_domain_deletion_enabled - return false if params[:delete][:verified] == true + return false if true?(params[:delete][:verified]) true end @@ -51,5 +51,9 @@ module Actions end true end + + def true?(obj) + obj.to_s.downcase == 'true' + end end end diff --git a/app/interactions/actions/domain_update.rb b/app/interactions/actions/domain_update.rb index 40b7876f6..ff6eccce2 100644 --- a/app/interactions/actions/domain_update.rb +++ b/app/interactions/actions/domain_update.rb @@ -14,6 +14,7 @@ module Actions assign_new_registrant if params[:registrant] assign_relational_modifications assign_requested_statuses + ::Actions::BaseAction.maybe_attach_legal_doc(domain, params[:legal_document]) commit @@ -45,6 +46,10 @@ module Actions def assign_new_registrant domain.add_epp_error('2306', nil, nil, %i[registrant cannot_be_missing]) unless params[:registrant][:code] + contact_code = params[:registrant][:code] + contact = Contact.find_by(code: contact_code) + validate_email(contact.email) if contact + regt = Registrant.find_by(code: params[:registrant][:code]) unless regt domain.add_epp_error('2303', 'registrant', params[:registrant], %i[registrant not_found]) @@ -120,9 +125,35 @@ module Actions @dnskeys << { id: dnkey.id, _destroy: 1 } if dnkey end + def start_validate_email(props) + contact = Contact.find_by(code: props[0][:contact_code]) + + return if contact.nil? + + validate_email(contact.email) + end + + def validate_email(email) + return true if Rails.env.test? + + %i[regex mx].each do |m| + result = Actions::SimpleMailValidator.run(email: email, level: m) + next if result + + err_text = "email #{email} didn't pass validation" + domain.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}") + @error = true + return + end + + true + end + def assign_admin_contact_changes props = gather_domain_contacts(params[:contacts].select { |c| c[:type] == 'admin' }) + start_validate_email(props) if props.present? + if props.any? && domain.admin_change_prohibited? domain.add_epp_error('2304', 'admin', DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation)) @@ -136,6 +167,8 @@ module Actions props = gather_domain_contacts(params[:contacts].select { |c| c[:type] == 'tech' }, admin: false) + start_validate_email(props) if props.present? + if props.any? && domain.tech_change_prohibited? domain.add_epp_error('2304', 'tech', DomainStatus::SERVER_TECH_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation)) @@ -173,7 +206,7 @@ module Actions domain.add_epp_error('2306', 'contact', code, %i[domain_contacts admin_contact_can_be_only_private_person]) else - add ? { contact_id: obj.id, contact_code_cache: obj.code } : { id: obj.id, _destroy: 1 } + add ? { contact_id: obj.id, contact_code: obj.code } : { id: obj.id, _destroy: 1 } end end @@ -208,7 +241,7 @@ module Actions def verify_registrant_change? return validate_dispute_case if params[:reserved_pw] - return false if !@changes_registrant || params[:registrant][:verified] == true + return false if !@changes_registrant || true?(params[:registrant][:verified]) return true unless domain.disputed? domain.add_epp_error('2304', nil, nil, 'Required parameter missing; reservedpw element ' \ @@ -250,5 +283,9 @@ module Actions false end + + def true?(obj) + obj.to_s.downcase == 'true' + end end end diff --git a/app/interactions/actions/email_check.rb b/app/interactions/actions/email_check.rb index 4b026ec2e..6631569f8 100644 --- a/app/interactions/actions/email_check.rb +++ b/app/interactions/actions/email_check.rb @@ -50,21 +50,21 @@ module Actions end def save_result(result) - if !result.success && @check_level == "mx" + contacts = Contact.where(email: email) + + if !result.success && @check_level == 'mx' result_validation = Actions::AAndAaaaEmailValidation.call(email: @email, value: 'A') - output_a_and_aaaa_validation_results(email: @email, - result: result_validation, - type: 'A') + output_a_and_aaaa_validation_results(email: @email, result: result_validation, type: 'A') result_validation = Actions::AAndAaaaEmailValidation.call(email: @email, value: 'AAAA') if result_validation.empty? - output_a_and_aaaa_validation_results(email: @email, - result: result_validation, - type: 'AAAA') + output_a_and_aaaa_validation_results(email: @email, result: result_validation, type: 'AAAA') + result.success = result_validation.present? + end - result_validation.present? ? result.success = true : result.success = false - validation_eventable.validation_events.create(validation_event_attrs(result)) - else - validation_eventable.validation_events.create(validation_event_attrs(result)) + contacts.find_in_batches(batch_size: 500) do |contact_batches| + contact_batches.each do |contact| + contact.validation_events.create(validation_event_attrs(result)) + end end rescue ActiveRecord::RecordNotSaved logger.info "Cannot save validation result for #{log_object_id}" @@ -92,8 +92,7 @@ module Actions when 'AAAA' ress = dns.getresources domain, Resolv::DNS::Resource::IN::AAAA end - - result = ress.map { |r| r.address } + result = ress.map(&:address) end result diff --git a/app/interactions/actions/invoice_cancel.rb b/app/interactions/actions/invoice_cancel.rb new file mode 100644 index 000000000..2f0a77894 --- /dev/null +++ b/app/interactions/actions/invoice_cancel.rb @@ -0,0 +1,15 @@ +module Actions + class InvoiceCancel + attr_reader :invoice + + def initialize(invoice) + @invoice = invoice + end + + def call + return false unless @invoice.can_be_cancelled? + + @invoice.update(cancelled_at: Time.zone.now) + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb b/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb index 3c2579890..f913d68b9 100644 --- a/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb +++ b/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb @@ -6,7 +6,6 @@ module Domains domain.statuses += domain.admin_store_statuses_history || [] domain.statuses.uniq! - domain.statuses_before_force_delete = nil domain.force_delete_domain_statuses_history = nil domain.admin_store_statuses_history = nil domain.save(validate: false) diff --git a/app/interactions/domains/force_delete/notify_registrar.rb b/app/interactions/domains/force_delete/notify_registrar.rb index e4aa48976..9cb91277d 100644 --- a/app/interactions/domains/force_delete/notify_registrar.rb +++ b/app/interactions/domains/force_delete/notify_registrar.rb @@ -6,18 +6,26 @@ module Domains end def notify_without_email - domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date)) + template = I18n.t('force_delete_set_on_domain', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date) + + return if domain.registrar&.notifications&.last&.text&.include? template + + domain.registrar.notifications.create!(text: template) end def notify_with_email - domain.registrar.notifications.create!(text: I18n.t('force_delete_auto_email', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date, - email: email)) + template = I18n.t('force_delete_auto_email', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date, + email: email) + + return if domain.registrar&.notifications&.last&.text&.include? template + + domain.registrar.notifications.create!(text: template) end end end diff --git a/app/interactions/domains/force_delete/prepare_domain.rb b/app/interactions/domains/force_delete/prepare_domain.rb index e45f7969c..116957a87 100644 --- a/app/interactions/domains/force_delete/prepare_domain.rb +++ b/app/interactions/domains/force_delete/prepare_domain.rb @@ -7,7 +7,6 @@ module Domains def execute domain.force_delete_domain_statuses_history = domain.statuses - domain.statuses_before_force_delete = domain.statuses domain.statuses |= STATUSES_TO_SET domain.save(validate: false) end diff --git a/app/interactions/domains/force_delete_email/base.rb b/app/interactions/domains/force_delete_email/base.rb index 04e7dde5d..d75749b50 100644 --- a/app/interactions/domains/force_delete_email/base.rb +++ b/app/interactions/domains/force_delete_email/base.rb @@ -31,18 +31,21 @@ module Domains def before_execute_force_delete(domain) if domain.force_delete_scheduled? && !domain.status_notes[DomainStatus::FORCE_DELETE].nil? added_additional_email_into_notes(domain) - notify_registrar(domain) else process_force_delete(domain) end end def notify_registrar(domain) - domain.registrar.notifications.create!(text: I18n.t('force_delete_auto_email', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date, - email: domain.status_notes[DomainStatus::FORCE_DELETE])) + template = I18n.t('force_delete_auto_email', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date, + email: domain.status_notes[DomainStatus::FORCE_DELETE]) + + return if domain.registrar.notifications.last.text.include? template + + domain.registrar.notifications.create!(text: template) end def process_force_delete(domain) @@ -56,6 +59,8 @@ module Domains def added_additional_email_into_notes(domain) return if domain.status_notes[DomainStatus::FORCE_DELETE].include? email + # notify_registrar(domain) + domain.status_notes[DomainStatus::FORCE_DELETE].concat(" #{email}") domain.save(validate: false) end diff --git a/app/jobs/check_force_delete_job.rb b/app/jobs/check_force_delete_job.rb new file mode 100644 index 000000000..5fb3408f1 --- /dev/null +++ b/app/jobs/check_force_delete_job.rb @@ -0,0 +1,43 @@ +class CheckForceDeleteJob < ApplicationJob + def perform(contact_ids) + contacts = Contact.find(contact_ids) + + contacts.each do |contact| + email = contact.email + + if contact.need_to_start_force_delete? + Domains::ForceDeleteEmail::Base.run(email: email) + elsif contact.need_to_lift_force_delete? + domain_list(email).each { |domain| refresh_status_notes(contact, domain) } + end + end + end + + private + + def refresh_status_notes(contact, domain) + force_delete_emails = domain.status_notes[DomainStatus::FORCE_DELETE] + return unless force_delete_emails + + force_delete_emails.slice!(contact.email_history) + force_delete_emails.lstrip! + domain.save(validate: false) + + notify_registrar(domain) unless force_delete_emails.empty? + end + + def domain_list(email) + domain_contacts = Contact.where(email: email).map(&:domain_contacts).flatten + registrant_ids = Registrant.where(email: email).pluck(:id) + + (domain_contacts.map(&:domain).flatten + Domain.where(registrant_id: registrant_ids)).uniq + end + + def notify_registrar(domain) + domain.registrar.notifications.create!(text: I18n.t('force_delete_auto_email', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date, + email: domain.status_notes[DomainStatus::FORCE_DELETE])) + end +end diff --git a/app/jobs/delete_monthly_invoices_job.rb b/app/jobs/delete_monthly_invoices_job.rb new file mode 100644 index 000000000..daf79827a --- /dev/null +++ b/app/jobs/delete_monthly_invoices_job.rb @@ -0,0 +1,10 @@ +class DeleteMonthlyInvoicesJob < ApplicationJob + queue_as :default + + def perform + @month = Time.zone.now - 1.month + invoices = Invoice.where(monthly_invoice: true, issue_date: @month.end_of_month.to_date, + in_directo: false, e_invoice_sent_at: nil) + invoices.delete_all + end +end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 43a537ade..28a06007a 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -1,59 +1,31 @@ class DirectoInvoiceForwardJob < ApplicationJob def perform(monthly: false, dry: false) - @dry = dry - (@month = Time.zone.now - 1.month) if monthly + data = nil - @client = new_directo_client - monthly ? send_monthly_invoices : send_receipts + if monthly + @month = Time.zone.now - 1.month + data = collect_monthly_data + else + data = collect_receipts_data + end + + EisBilling::SendDataToDirecto.send_request(object_data: data, monthly: monthly, dry: dry) end - def new_directo_client - DirectoApi::Client.new(ENV['directo_invoice_url'], Setting.directo_sales_agent, - Setting.directo_receipt_payment_term) - end - - def send_receipts + def collect_receipts_data unsent_invoices = Invoice.where(in_directo: false).non_cancelled + collected_data = [] - Rails.logger.info("[DIRECTO] Trying to send #{unsent_invoices.count} prepayment invoices") unsent_invoices.each do |invoice| unless valid_invoice_conditions?(invoice) Rails.logger.info "[DIRECTO] Invoice #{invoice.number} has been skipped" next end - @client.invoices.add_with_schema(invoice: invoice.as_directo_json, schema: 'prepayment') + + collected_data << invoice.as_directo_json end - sync_with_directo - end - - def send_monthly_invoices - Registrar.where.not(test_registrar: true).find_each do |registrar| - next unless registrar.cash_account - - @client = new_directo_client - send_invoice_for_registrar(registrar) - end - end - - def send_invoice_for_registrar(registrar) - summary = registrar.monthly_summary(month: @month) - @client.invoices.add_with_schema(invoice: summary, schema: 'summary') unless summary.nil? - - sync_with_directo if @client.invoices.count.positive? - end - - def assign_monthly_numbers - raise 'Directo Counter is going to be out of period!' if directo_counter_exceedable?(@client.invoices.count) - - min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) - directo_number = [Setting.directo_monthly_number_last.presence.try(:to_i), - min_directo].compact.max || 0 - - @client.invoices.each do |inv| - directo_number += 1 - inv.number = directo_number - end + collected_data end def valid_invoice_conditions?(invoice) @@ -67,28 +39,17 @@ class DirectoInvoiceForwardJob < ApplicationJob true end - def sync_with_directo - assign_monthly_numbers if @month - Rails.logger.info("[Directo] - attempting to send following XML:\n #{@client.invoices.as_xml}") - return if @dry + def collect_monthly_data + registrars_data = [] - res = @client.invoices.deliver(ssl_verify: false) - process_directo_response(res.body, @client.invoices.as_xml) - rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, - EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError - Rails.logger.info('[Directo] Failed to communicate via API') - end - - def process_directo_response(xml, req) - Rails.logger.info "[Directo] - Responded with body: #{xml}" - Nokogiri::XML(xml).css('Result').each do |res| - if @month - mark_invoice_as_sent(res: res, req: req) - else - inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) - mark_invoice_as_sent(invoice: inv, res: res, req: req) - end + Registrar.where.not(test_registrar: true).find_each do |registrar| + registrars_data << { + registrar: registrar, + registrar_summery: registrar.monthly_summary(month: @month), + } end + + registrars_data end def mark_invoice_as_sent(invoice: nil, res:, req:) diff --git a/app/jobs/directo_invoice_forward_legacy_job.rb b/app/jobs/directo_invoice_forward_legacy_job.rb new file mode 100644 index 000000000..5534b3c9b --- /dev/null +++ b/app/jobs/directo_invoice_forward_legacy_job.rb @@ -0,0 +1,125 @@ +class DirectoInvoiceForwardLegacyJob < ApplicationJob + def perform(monthly: false, dry: false) + @dry = dry + (@month = Time.zone.now - 1.month) if monthly + + @client = new_directo_client + monthly ? send_monthly_invoices : send_receipts + end + + def new_directo_client + DirectoApi::Client.new(ENV['directo_invoice_url'], Setting.directo_sales_agent, + Setting.directo_receipt_payment_term) + end + + def send_receipts + unsent_invoices = Invoice.where(in_directo: false).non_cancelled + + Rails.logger.info("[DIRECTO] Trying to send #{unsent_invoices.count} prepayment invoices") + unsent_invoices.each do |invoice| + unless valid_invoice_conditions?(invoice) + Rails.logger.info "[DIRECTO] Invoice #{invoice.number} has been skipped" + next + end + + @client.invoices.add_with_schema(invoice: invoice.as_directo_json, schema: 'prepayment') + end + + sync_with_directo + end + + def send_monthly_invoices + Registrar.where.not(test_registrar: true).find_each do |registrar| + next unless registrar.cash_account + + @client = new_directo_client + send_invoice_for_registrar(registrar) + end + end + + def send_invoice_for_registrar(registrar) + summary = registrar.monthly_summary(month: @month) + @client.invoices.add_with_schema(invoice: summary, schema: 'summary') unless summary.nil? + + sync_with_directo if @client.invoices.count.positive? + end + + def assign_monthly_numbers + raise 'Directo Counter is going to be out of period!' if directo_counter_exceedable?(@client.invoices.count) + + min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) + directo_number = [Setting.directo_monthly_number_last.presence.try(:to_i), + min_directo].compact.max || 0 + + @client.invoices.each do |inv| + directo_number += 1 + inv.number = directo_number + end + end + + def valid_invoice_conditions?(invoice) + if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || + invoice.account_activity.bank_transaction.sum.nil? || + invoice.account_activity.bank_transaction.sum != invoice.total + return false + + end + + true + end + + def sync_with_directo + assign_monthly_numbers if @month + + Rails.logger.info("[Directo] - attempting to send following XML:\n #{@client.invoices.as_xml}") + return if @dry + + res = @client.invoices.deliver(ssl_verify: false) + process_directo_response(res.body, @client.invoices.as_xml) + rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, + EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError + Rails.logger.info('[Directo] Failed to communicate via API') + end + + def process_directo_response(xml, req) + Rails.logger.info "[Directo] - Responded with body: #{xml}" + Nokogiri::XML(xml).css('Result').each do |res| + if @month + mark_invoice_as_sent(res: res, req: req) + else + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + mark_invoice_as_sent(invoice: inv, res: res, req: req) + end + end + end + + def mark_invoice_as_sent(invoice: nil, res:, req:) + directo_record = Directo.new(response: res.as_json.to_h, + request: req, invoice_number: res.attributes['docid'].value.to_i) + if invoice + directo_record.item = invoice + invoice.update(in_directo: true) + else + update_directo_number(num: directo_record.invoice_number) + end + + directo_record.save! + end + + def update_directo_number(num:) + return unless num.to_i > Setting.directo_monthly_number_last.to_i + + Setting.directo_monthly_number_last = num.to_i + end + + def directo_counter_exceedable?(invoice_count) + min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) + max_directo = Setting.directo_monthly_number_max.presence.try(:to_i) + last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), + min_directo].compact.max || 0 + + return true if max_directo && max_directo < (last_directo + invoice_count) + + false + end +end diff --git a/app/jobs/nameserver_record_validation_job.rb b/app/jobs/nameserver_record_validation_job.rb index 7dd2211a4..f737c0f36 100644 --- a/app/jobs/nameserver_record_validation_job.rb +++ b/app/jobs/nameserver_record_validation_job.rb @@ -64,12 +64,11 @@ class NameserverRecordValidationJob < ApplicationJob nameserver.save end - def add_nameserver_to_failed(nameserver:, reason:) - if nameserver.validation_counter.nil? - nameserver.validation_counter = 1 - else - nameserver.validation_counter = nameserver.validation_counter + 1 - end + def add_nameserver_to_failed(nameserver:, reason:, result_reason:) + return cname_case_handle(nameserver: nameserver, reason: reason) if result_reason == 'cname' + + nameserver.validation_counter = 1 if nameserver.validation_counter.nil? + nameserver.validation_counter = nameserver.validation_counter + 1 nameserver.failed_validation_reason = reason nameserver.save @@ -77,15 +76,25 @@ class NameserverRecordValidationJob < ApplicationJob failed_log(text: reason, nameserver: nameserver, domain: nameserver.domain) if nameserver.failed_validation? end + def cname_case_handle(nameserver:, reason:) + nameserver.validation_datetime = Time.zone.now + nameserver.failed_validation_reason = reason + nameserver.save + + failed_log(text: reason, nameserver: nameserver, domain: nameserver.domain) + end + def parse_result(result, nameserver) domain = Domain.find(nameserver.domain_id) - text = "" + text = '' case result[:reason] when 'answer' - text = "No any answer comes from **#{nameserver.hostname}**. Nameserver not exist" + text = "DNS Server **#{nameserver.hostname}** not responding" when 'serial' - text = "Serial number for nameserver hostname **#{nameserver.hostname}** doesn't present. SOA validation failed." + text = "Serial number for nameserver hostname **#{nameserver.hostname}** of #{nameserver.domain.name} doesn't present in zone. SOA validation failed." + when 'cname' + text = "Warning: SOA record expected but CNAME found instead. This setup can lead to unexpected errors when using the domain: hostname - **#{nameserver.hostname}** of #{nameserver.domain.name}" when 'not found' text = "Seems nameserver hostname **#{nameserver.hostname}** doesn't exist" when 'exception' @@ -97,7 +106,7 @@ class NameserverRecordValidationJob < ApplicationJob end logger.info text - add_nameserver_to_failed(nameserver: nameserver, reason: text) + add_nameserver_to_failed(nameserver: nameserver, reason: text, result_reason: result[:reason]) false end diff --git a/app/jobs/send_e_invoice_job.rb b/app/jobs/send_e_invoice_job.rb index 33a2745c6..02cad898c 100644 --- a/app/jobs/send_e_invoice_job.rb +++ b/app/jobs/send_e_invoice_job.rb @@ -6,7 +6,8 @@ class SendEInvoiceJob < ApplicationJob invoice = Invoice.find_by(id: invoice_id) return unless need_to_process_invoice?(invoice: invoice, payable: payable) - process(invoice: invoice, payable: payable) + send_invoice_to_eis_billing(invoice: invoice, payable: payable) + invoice.update(e_invoice_sent_at: Time.zone.now) rescue StandardError => e log_error(invoice: invoice, error: e) raise e @@ -22,16 +23,9 @@ class SendEInvoiceJob < ApplicationJob true end - def process(invoice:, payable:) - invoice.to_e_invoice(payable: payable).deliver unless Rails.env.development? - invoice.update(e_invoice_sent_at: Time.zone.now) - log_success(invoice) - end - - def log_success(invoice) - id = invoice.try(:id) || invoice - message = "E-Invoice for an invoice with ID # #{id} was sent successfully" - logger.info message + def send_invoice_to_eis_billing(invoice:, payable:) + result = EisBilling::SendEInvoice.send_request(invoice: invoice, payable: payable) + logger.info result.body end def log_error(invoice:, error:) diff --git a/app/jobs/send_e_invoice_legacy_job.rb b/app/jobs/send_e_invoice_legacy_job.rb new file mode 100644 index 000000000..d9220388b --- /dev/null +++ b/app/jobs/send_e_invoice_legacy_job.rb @@ -0,0 +1,51 @@ +class SendEInvoiceLegacyJob < ApplicationJob + discard_on HTTPClient::TimeoutError + + def perform(invoice_id, payable: true) + logger.info "Started to process e-invoice for invoice_id #{invoice_id}" + invoice = Invoice.find_by(id: invoice_id) + return unless need_to_process_invoice?(invoice: invoice, payable: payable) + + process(invoice: invoice, payable: payable) + rescue StandardError => e + log_error(invoice: invoice, error: e) + raise e + end + + private + + def need_to_process_invoice?(invoice:, payable:) + logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}" + unprocessable = invoice.do_not_send_e_invoice? && (invoice.monthly_invoice ? true : payable) + return false if invoice.blank? + return false if unprocessable + + true + end + + def process(invoice:, payable:) + invoice.to_e_invoice(payable: payable).deliver unless Rails.env.development? + invoice.update(e_invoice_sent_at: Time.zone.now) + log_success(invoice) + end + + def log_success(invoice) + id = invoice.try(:id) || invoice + message = "E-Invoice for an invoice with ID # #{id} was sent successfully" + logger.info message + end + + def log_error(invoice:, error:) + id = invoice.try(:id) || invoice + message = <<~TEXT.squish + There was an error sending e-invoice for invoice with ID # #{id}. + The error message was the following: #{error} + This job will retry. + TEXT + logger.error message + end + + def logger + Rails.logger + end +end diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb new file mode 100644 index 000000000..af37b156e --- /dev/null +++ b/app/jobs/send_monthly_invoices_job.rb @@ -0,0 +1,147 @@ +class SendMonthlyInvoicesJob < ApplicationJob # rubocop:disable Metrics/ClassLength + queue_as :default + + def perform(dry: false) + @dry = dry + @month = Time.zone.now - 1.month + @directo_client = new_directo_client + @min_directo_num = Setting.directo_monthly_number_min.presence.try(:to_i) + @max_directo_num = Setting.directo_monthly_number_max.presence.try(:to_i) + + send_monthly_invoices + end + + def new_directo_client + DirectoApi::Client.new(ENV['directo_invoice_url'], Setting.directo_sales_agent, + Setting.directo_receipt_payment_term) + end + + # rubocop:disable Metrics/MethodLength + def send_monthly_invoices + Registrar.with_cash_accounts.find_each do |registrar| + summary = registrar.monthly_summary(month: @month) + next if summary.nil? + + invoice = registrar.monthly_invoice(month: @month) || create_invoice(summary, registrar) + next if invoice.nil? || @dry + + send_email_to_registrar(invoice: invoice, registrar: registrar) + send_e_invoice(invoice.id) + next if invoice.in_directo + + Rails.logger.info("[DIRECTO] Trying to send monthly invoice #{invoice.number}") + @directo_client = new_directo_client + directo_invoices = @directo_client.invoices.add_with_schema(invoice: summary, + schema: 'summary') + next unless directo_invoices.size.positive? + + directo_invoices.last.number = invoice.number + sync_with_directo + end + end + + # rubocop:enable Metrics/MethodLength + + def send_email_to_registrar(invoice:, registrar:) + InvoiceMailer.invoice_email(invoice: invoice, + recipient: registrar.billing_email) + .deliver_now + end + + def send_e_invoice(invoice_id) + SendEInvoiceLegacyJob.set(wait: 1.minute).perform_later(invoice_id, payable: false) + end + + def create_invoice(summary, registrar) + invoice = registrar.init_monthly_invoice(normalize(summary)) + invoice.number = assign_monthly_number + return unless invoice.save! + + update_monthly_invoice_number(num: invoice.number) + invoice + end + + def sync_with_directo + invoices_xml = @directo_client.invoices.as_xml + + Rails.logger.info("[Directo] - attempting to send following XML:\n #{invoices_xml}") + + res = @directo_client.invoices.deliver(ssl_verify: false) + process_directo_response(res.body, invoices_xml) + rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, + EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError + Rails.logger.info('[Directo] Failed to communicate via API') + end + + def assign_monthly_number + last_directo_num = [Setting.directo_monthly_number_last.presence.try(:to_i), + @min_directo_num].compact.max || 0 + raise 'Directo Counter is out of period!' if directo_counter_exceedable?(1, last_directo_num) + + last_directo_num + 1 + end + + def directo_counter_exceedable?(invoices_count, last_directo_num) + return true if @max_directo_num && @max_directo_num < (last_directo_num + invoices_count) + + false + end + + def process_directo_response(body, req) + Rails.logger.info "[Directo] - Responded with body: #{body}" + Nokogiri::XML(body).css('Result').each do |res| + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + mark_invoice_as_sent_to_directo(res: res, req: req, invoice: inv) + end + end + + def mark_invoice_as_sent_to_directo(res:, req:, invoice: nil) + directo_record = Directo.new(response: res.as_json.to_h, + request: req, invoice_number: res.attributes['docid'].value.to_i) + directo_record.item = invoice + invoice.update(in_directo: true) + + directo_record.save! + end + + def update_monthly_invoice_number(num:) + return unless num.to_i > Setting.directo_monthly_number_last.to_i + + Setting.directo_monthly_number_last = num.to_i + end + + private + + # rubocop:disable Metrics/MethodLength + def normalize(summary, lines: []) + sum = summary.dup + line_map = Hash.new 0 + sum['invoice_lines'].each { |l| line_map[l] += 1 } + + line_map.each_key do |count| + count['quantity'] = line_map[count] unless count['unit'].nil? + regex = /Domeenide ettemaks|Domains prepayment/ + count['quantity'] = -1 if count['description'].match?(regex) + lines << count + end + + sum['invoice_lines'] = summarize_lines(lines) + sum + end + # rubocop:enable Metrics/MethodLength + + def summarize_lines(invoice_lines, lines: []) + line_map = Hash.new 0 + invoice_lines.each do |l| + hash = l.with_indifferent_access.except(:start_date, :end_date) + line_map[hash] += 1 + end + + line_map.each_key do |count| + count['price'] = (line_map[count] * count['price'].to_f).round(3) unless count['price'].nil? + lines << count + end + + lines + end +end diff --git a/app/jobs/validate_dnssec_job.rb b/app/jobs/validate_dnssec_job.rb index 623513768..69168efbb 100644 --- a/app/jobs/validate_dnssec_job.rb +++ b/app/jobs/validate_dnssec_job.rb @@ -36,7 +36,7 @@ class ValidateDnssecJob < ApplicationJob domain.nameservers.each do |n| next unless n.validated? - validate(hostname: n.hostname, domain: domain) + validate(nameserver: n, domain: domain) notify_contacts(domain) logger.info "----------------------------" @@ -54,25 +54,26 @@ class ValidateDnssecJob < ApplicationJob # ContactNotification.notify_tech_contact(domain: domain, reason: 'dnssec') end - def validate(hostname:, domain:, type: 'DNSKEY', klass: 'IN') - resolver = prepare_validator(hostname) + def validate(nameserver:, domain:, type: 'DNSKEY', klass: 'IN') + resolver = prepare_validator(nameserver.hostname) answer = resolver.query(domain.name, type, klass) - return logger.info "no any data for #{domain.name} | hostname - #{hostname}" if answer.nil? + return logger.info "no any data for #{domain.name} | hostname - #{nameserver.hostname}" if answer.nil? logger.info "-----------" - logger.info "data for domain name - #{domain.name} | hostname - #{hostname}" + logger.info "data for domain name - #{domain.name} | hostname - #{nameserver.hostname}" logger.info "-----------" response_container = parse_response(answer) - compare_dnssec_data(response_container: response_container, domain: domain) + compare_dnssec_data(response_container: response_container, domain: domain, nameserver: nameserver) rescue Exception => e - logger.error "#{e.message} - domain name: #{domain.name} - hostname: #{hostname}" + logger.error "#{e.message} - domain name: #{domain.name} - hostname: #{nameserver.hostname}" + nameserver.update(failed_validation_reason: "#{e.message} - domain name: #{domain.name} - hostname: #{nameserver.hostname}") nil end - def compare_dnssec_data(response_container:, domain:) + def compare_dnssec_data(response_container:, domain:, nameserver:) domain.dnskeys.each do |key| next unless key.flags.to_s == '257' next if key.validation_datetime.present? @@ -82,11 +83,15 @@ class ValidateDnssecJob < ApplicationJob if flag key.validation_datetime = Time.zone.now + key.failed_validation_reason = nil key.save + nameserver.failed_validation_reason = nil + nameserver.save logger.info text + " ------->> succesfully!" else - logger.info text + " ------->> not found in zone!" + key.update!(failed_validation_reason: text + " not found in zone! Domain name - #{domain.name}. Hostname - #{nameserver.hostname}") + logger.info text + " ------->> not found in zone! Domain name - #{domain.name}. Hostname - #{nameserver.hostname}" end end end @@ -133,10 +138,11 @@ class ValidateDnssecJob < ApplicationJob inner_resolver.nameserver = nameserver inner_resolver.packet_timeout = timeouts.to_i inner_resolver.query_timeout = timeouts.to_i - resolver = Dnsruby::Recursor.new(inner_resolver) - resolver.dnssec = true + # resolver = Dnsruby::Recursor.new(inner_resolver) + # resolver.dnssec = true - resolver + # resolver + inner_resolver end def logger diff --git a/app/jobs/verify_emails_job.rb b/app/jobs/verify_emails_job.rb index 4b9b98fb7..441a46569 100644 --- a/app/jobs/verify_emails_job.rb +++ b/app/jobs/verify_emails_job.rb @@ -1,8 +1,13 @@ class VerifyEmailsJob < ApplicationJob discard_on StandardError - def perform(contact:, check_level: 'mx') - contact_not_found(contact.id) unless contact + def perform(email:, check_level: 'mx') + contact = Contact.find_by(email: email) + + return Rails.logger.info "No found #{email} contact" if contact.nil? + + return unless filter_check_level(contact) + validate_check_level(check_level) action = Actions::EmailCheck.new(email: contact.email, validation_eventable: contact, @@ -15,10 +20,6 @@ class VerifyEmailsJob < ApplicationJob private - def contact_not_found(contact_id) - raise StandardError, "Contact with contact_id #{contact_id} not found" - end - def validate_check_level(check_level) return if valid_check_levels.include? check_level @@ -32,4 +33,13 @@ class VerifyEmailsJob < ApplicationJob def valid_check_levels ValidationEvent::VALID_CHECK_LEVELS end + + def filter_check_level(contact) + return true unless contact.validation_events.exists? + + data = contact.validation_events.order(created_at: :asc).last + return true if data.successful? && data.created_at < (Time.zone.now - ValidationEvent::VALIDATION_PERIOD) + + !(data.failed? && data.event_data['check_level'] == 'regex') + end end diff --git a/app/mailers/invoice_mailer.rb b/app/mailers/invoice_mailer.rb index a9d544d63..95b7fefd6 100644 --- a/app/mailers/invoice_mailer.rb +++ b/app/mailers/invoice_mailer.rb @@ -4,6 +4,7 @@ class InvoiceMailer < ApplicationMailer subject = default_i18n_subject(invoice_number: invoice.number) subject << I18n.t('invoice.already_paid') if paid + subject << I18n.t('invoice.monthly_invoice') if invoice.monthly_invoice attachments["invoice-#{invoice.number}.pdf"] = invoice.as_pdf mail(to: recipient, subject: subject) end diff --git a/app/models/ability.rb b/app/models/ability.rb index caca24524..31543a586 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -34,9 +34,11 @@ class Ability if @user.registrar.api_ip_white?(@ip) can :manage, Depp::Contact can :manage, :xml_console - can :manage, Depp::Domain + can :manage, Depp::Domain end + can :manage, Account + # Poll can :manage, :poll @@ -65,12 +67,13 @@ class Ability can(:update, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:renew, Epp::Contact) - can(:transfer, Epp::Contact) + can(:transfer, Epp::Contact) can(:view_password, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } end def billing # Registrar/api_user dynamic role can(:manage, Invoice) { |i| i.buyer_id == @user.registrar_id } + can :manage, Account can :manage, :deposit can :read, AccountActivity can :manage, :balance_auto_reload @@ -95,6 +98,7 @@ class Ability can :manage, User can :manage, ApiUser can :manage, AdminUser + can :manage, Auction can :manage, Certificate can :manage, LegalDocument can :manage, BankStatement diff --git a/app/models/action.rb b/app/models/action.rb index 8a822f867..03c8e9fe8 100644 --- a/app/models/action.rb +++ b/app/models/action.rb @@ -28,14 +28,20 @@ class Action < ApplicationRecord end def to_non_available_contact_codes - return [] unless bulk_action? + return [serialized_contact(contact)] unless bulk_action? subactions.map do |a| - { - code: a.contact.code, - avail: 0, - reason: 'in use', - } + serialized_contact(a.contact) end end + + private + + def serialized_contact(contact) + { + code: contact.code, + avail: 0, + reason: 'in use', + } + end end diff --git a/app/models/admin_domain_contact.rb b/app/models/admin_domain_contact.rb index 7ccf3efcb..99f0d02da 100644 --- a/app/models/admin_domain_contact.rb +++ b/app/models/admin_domain_contact.rb @@ -6,7 +6,7 @@ class AdminDomainContact < DomainContact skipped_domains = [] admin_contacts = where(contact: current_contact) - admin_contacts.each do |admin_contact| + admin_contacts.includes(:domain).find_each do |admin_contact| if admin_contact.domain.bulk_update_prohibited? skipped_domains << admin_contact.domain.name next diff --git a/app/models/api_user.rb b/app/models/api_user.rb index 8ae131a6e..dc5cff0cc 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -30,11 +30,11 @@ class ApiUser < User alias_attribute :login, :username - SUPER = 'super' - EPP = 'epp' - BILLING = 'billing' + SUPER = 'super'.freeze + EPP = 'epp'.freeze + BILLING = 'billing'.freeze - ROLES = %w(super epp billing) # should not match to admin roles + ROLES = %w[super epp billing].freeze # should not match to admin roles def ability @ability ||= Ability.new(self) @@ -72,8 +72,9 @@ class ApiUser < User def linked_users self.class.where(identity_code: identity_code) - .where("identity_code IS NOT NULL AND identity_code != ''") - .where.not(id: id) + .where("identity_code IS NOT NULL AND identity_code != ''") + .where.not(id: id) + .includes(:registrar) end def linked_with?(another_api_user) diff --git a/app/models/auction.rb b/app/models/auction.rb index 791184d60..465a827ec 100644 --- a/app/models/auction.rb +++ b/app/models/auction.rb @@ -9,11 +9,30 @@ class Auction < ApplicationRecord domain_not_registered: 'domain_not_registered', } + enum platform: %i[auto manual] + PENDING_STATUSES = [statuses[:started], statuses[:awaiting_payment], statuses[:payment_received]].freeze + private_constant :PENDING_STATUSES + scope :with_status, ->(status) { + where(status: status) if status.present? + } + + scope :with_start_created_at_date, ->(start_created_at) { + where('created_at >= ?', start_created_at) if start_created_at.present? + } + + scope :with_end_created_at_date, ->(end_created_at) { + where('created_at <= ?', end_created_at) if end_created_at.present? + } + + scope :with_domain_name, ->(domain_name) { + where('domain ilike ?', "%#{domain_name.strip}%") if domain_name.present? + } + def self.pending(domain_name) find_by(domain: domain_name.to_s, status: PENDING_STATUSES) end diff --git a/app/models/balance_auto_reload_types/threshold.rb b/app/models/balance_auto_reload_types/threshold.rb index d55cb977a..8bb494ae5 100644 --- a/app/models/balance_auto_reload_types/threshold.rb +++ b/app/models/balance_auto_reload_types/threshold.rb @@ -1,6 +1,7 @@ module BalanceAutoReloadTypes class Threshold include ActiveModel::Model + include ActiveModel::Validations attr_accessor :amount, :threshold @@ -11,8 +12,9 @@ module BalanceAutoReloadTypes Setting.minimum_deposit end - def as_json(options) + def as_json(options = nil) { name: name }.merge(super) + .except('errors', 'validation_context') end private diff --git a/app/models/bank_transaction.rb b/app/models/bank_transaction.rb index 68b48995f..ef2ab3370 100644 --- a/app/models/bank_transaction.rb +++ b/app/models/bank_transaction.rb @@ -33,10 +33,10 @@ class BankTransaction < ApplicationRecord return unless autobindable? channel = manual ? 'admin_payment' : 'system_payment' - create_internal_payment_record(channel: channel, invoice: invoice, registrar: registrar) + create_internal_payment_record(invoice: invoice, registrar: registrar, channel: channel) end - def create_internal_payment_record(channel: nil, invoice:, registrar:) + def create_internal_payment_record(invoice:, registrar:, channel: nil) if channel.nil? create_activity(invoice.buyer, invoice) return @@ -47,9 +47,12 @@ class BankTransaction < ApplicationRecord if create_activity(registrar, invoice) payment_order.paid! + EisBilling::SendInvoiceStatus.send_info(invoice_number: invoice.number, + status: 'paid') else payment_order.update(notes: 'Failed to create activity', status: 'failed') end + invoice end def bind_invoice(invoice_no, manual: false) @@ -62,8 +65,8 @@ class BankTransaction < ApplicationRecord validate_invoice_data(invoice) return if errors.any? - create_internal_payment_record(channel: (manual ? 'admin_payment' : nil), invoice: invoice, - registrar: invoice.buyer) + create_internal_payment_record(invoice: invoice, registrar: invoice.buyer, + channel: (manual ? 'admin_payment' : nil)) end def validate_invoice_data(invoice) diff --git a/app/models/billing/reference_no.rb b/app/models/billing/reference_no.rb index fbb9218c4..050db73d4 100644 --- a/app/models/billing/reference_no.rb +++ b/app/models/billing/reference_no.rb @@ -4,8 +4,8 @@ module Billing MULTI_REGEXP = /(\d{2,20})/ def self.generate - base = Base.generate - "#{base}#{base.check_digit}" + result = EisBilling::GetReferenceNumber.send_request + JSON.parse(result.body)['reference_number'] end def self.valid?(ref) diff --git a/app/models/bulk_action.rb b/app/models/bulk_action.rb deleted file mode 100644 index 9c98ee2db..000000000 --- a/app/models/bulk_action.rb +++ /dev/null @@ -1 +0,0 @@ -class BulkAction < Action; end diff --git a/app/models/concerns/contact/disclosable.rb b/app/models/concerns/contact/disclosable.rb deleted file mode 100644 index a61b240b1..000000000 --- a/app/models/concerns/contact/disclosable.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Contact::Disclosable - extend ActiveSupport::Concern - - class_methods do - attr_accessor :disclosable_attributes - end - - included do - self.disclosable_attributes = %w[name email] - validate :validate_disclosed_attributes - end - - private - - def validate_disclosed_attributes - return if disclosed_attributes.empty? - - has_undisclosable_attributes = (disclosed_attributes - self.class.disclosable_attributes) - .any? - errors.add(:disclosed_attributes, :invalid) if has_undisclosable_attributes - end -end diff --git a/app/models/concerns/email_verifable.rb b/app/models/concerns/email_verifable.rb index 4f9b4ffeb..2d2191d8f 100644 --- a/app/models/concerns/email_verifable.rb +++ b/app/models/concerns/email_verifable.rb @@ -18,9 +18,7 @@ module EmailVerifable def need_to_start_force_delete? flag = false ValidationEvent::INVALID_EVENTS_COUNT_BY_LEVEL.each do |level, count| - if validation_events.count >= count && validate_email_data(level: level, count: count) - flag = true - end + flag = true if validation_events.count >= count && validate_email_data(level: level, count: count) end flag diff --git a/app/models/concerns/invoice/cancellable.rb b/app/models/concerns/invoice/cancellable.rb index 8c9e142a8..9b1c6435b 100644 --- a/app/models/concerns/invoice/cancellable.rb +++ b/app/models/concerns/invoice/cancellable.rb @@ -5,12 +5,22 @@ module Invoice::Cancellable scope :non_cancelled, -> { where(cancelled_at: nil) } end + def can_be_cancelled? + unless cancellable? + errors.add(:base, :invoice_status_prohibits_operation) + return false + end + + true + end + def cancellable? unpaid? && not_cancelled? end def cancel raise 'Invoice cannot be cancelled' unless cancellable? + update!(cancelled_at: Time.zone.now) end diff --git a/app/models/concerns/invoice/payable.rb b/app/models/concerns/invoice/payable.rb index 6e2cc19b4..855ea8f41 100644 --- a/app/models/concerns/invoice/payable.rb +++ b/app/models/concerns/invoice/payable.rb @@ -15,6 +15,8 @@ module Invoice::Payable end def receipt_date + return unless paid? + account_activity.created_at.to_date end diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index 76535ce12..fc1defe9a 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -1,22 +1,27 @@ -module Registrar::BookKeeping +module Registrar::BookKeeping # rubocop:disable Metrics/ModuleLength extend ActiveSupport::Concern DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI', 'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze + included do + scope :with_cash_accounts, (lambda do + joins(:accounts).where('accounts.account_type = ? AND test_registrar != ?', + Account::CASH, + true) + end) + end + def monthly_summary(month:) activities = monthly_activites(month) return unless activities.any? invoice = { - 'number': 1, - 'customer': compose_directo_customer, + 'number': 1, 'customer': compose_directo_customer, 'language': language == 'en' ? 'ENG' : '', 'currency': activities.first.currency, 'date': month.end_of_month.strftime('%Y-%m-%d') }.as_json - invoice['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) - invoice end @@ -55,20 +60,25 @@ module Registrar::BookKeeping .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) end + def monthly_invoice(month:) + invoices.where(monthly_invoice: true, issue_date: month.end_of_month.to_date, + cancelled_at: nil).first + end + def new_monthly_invoice_line(activity:, duration: nil) price = load_price(activity) line = { 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], 'quantity': 1, 'unit': language == 'en' ? 'pc' : 'tk', - } + }.with_indifferent_access finalize_invoice_line(line, price: price, duration: duration, activity: activity) end def finalize_invoice_line(line, price:, activity:, duration:) yearly = price.duration.in_years.to_i >= 1 - line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i) : price.price.amount + line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i).to_f : price.price.amount.to_f line['description'] = description_in_language(price: price, yearly: yearly) add_product_timeframe(line: line, activity: activity, duration: duration) if duration.present? && (duration > 1) @@ -79,15 +89,16 @@ module Registrar::BookKeeping def add_product_timeframe(line:, activity:, duration:) create_time = activity.created_at line['start_date'] = (create_time + (duration - 1).year).end_of_month.strftime('%Y-%m-%d') - line['end_date'] = (create_time + (duration - 1).year + 1).end_of_month.strftime('%Y-%m-%d') + line['end_date'] = (create_time + duration.year).end_of_month.strftime('%Y-%m-%d') end def description_in_language(price:, yearly:) timeframe_string = yearly ? 'yearly' : 'monthly' locale_string = "registrar.invoice_#{timeframe_string}_product_description" + length = yearly ? price.duration.in_years.to_i : price.duration.in_months.to_i I18n.with_locale(language == 'en' ? 'en' : 'et') do - I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.in_years.to_i) + I18n.t(locale_string, tld: ".#{price.zone_name}", length: length) end end diff --git a/app/models/contact.rb b/app/models/contact.rb index 676b0da87..e4628e3c0 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -7,7 +7,6 @@ class Contact < ApplicationRecord include UserEvents include Contact::Transferable include Contact::Identical - include Contact::Disclosable include Contact::Archivable include EmailVerifable @@ -16,7 +15,7 @@ class Contact < ApplicationRecord has_many :domain_contacts has_many :domains, through: :domain_contacts has_many :legal_documents, as: :documentable - has_many :validation_events, as: :validation_eventable + 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 @@ -55,7 +54,7 @@ class Contact < ApplicationRecord validates :phone, presence: true, e164: true, phone: true - validate :correct_email_format, if: proc { |c| c.will_save_change_to_email? } + # validate :correct_email_format, if: proc { |c| c.will_save_change_to_email? } validates :code, uniqueness: { message: :epp_id_taken }, @@ -86,41 +85,41 @@ class Contact < ApplicationRecord self.ignored_columns = %w[legacy_id legacy_history_id] - ORG = 'org' - PRIV = 'priv' + 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' + PASSPORT = 'passport'.freeze # # STATUSES # # Requests to delete the object MUST be rejected. - CLIENT_DELETE_PROHIBITED = 'clientDeleteProhibited' - SERVER_DELETE_PROHIBITED = 'serverDeleteProhibited' + CLIENT_DELETE_PROHIBITED = 'clientDeleteProhibited'.freeze + SERVER_DELETE_PROHIBITED = 'serverDeleteProhibited'.freeze # Requests to transfer the object MUST be rejected. - CLIENT_TRANSFER_PROHIBITED = 'clientTransferProhibited' - SERVER_TRANSFER_PROHIBITED = 'serverTransferProhibited' + 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' + 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' + OK = 'ok'.freeze # Requests to update the object (other than to remove this status) MUST be rejected. - CLIENT_UPDATE_PROHIBITED = 'clientUpdateProhibited' - SERVER_UPDATE_PROHIBITED = 'serverUpdateProhibited' + 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 @@ -135,16 +134,16 @@ class Contact < ApplicationRecord # 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' + PENDING_CREATE = 'pendingCreate'.freeze # "pendingTransfer" status MUST NOT be combined with either # "clientTransferProhibited" or "serverTransferProhibited" status. - PENDING_TRANSFER = 'pendingTransfer' + PENDING_TRANSFER = 'pendingTransfer'.freeze # "pendingUpdate" status MUST NOT be combined with either # "clientUpdateProhibited" or "serverUpdateProhibited" status. - PENDING_UPDATE = 'pendingUpdate' + PENDING_UPDATE = 'pendingUpdate'.freeze # "pendingDelete" MUST NOT be combined with either # "clientDeleteProhibited" or "serverDeleteProhibited" status. - PENDING_DELETE = 'pendingDelete' + PENDING_DELETE = 'pendingDelete'.freeze STATUSES = [ CLIENT_DELETE_PROHIBITED, SERVER_DELETE_PROHIBITED, @@ -152,18 +151,18 @@ class Contact < ApplicationRecord 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 - ] + SERVER_TRANSFER_PROHIBITED, + ].freeze # # END OF STATUSES # @@ -361,7 +360,7 @@ class Contact < ApplicationRecord @desc[dom.name][:roles] << :registrant end - domain_contacts.each do |dc| + 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 @@ -389,6 +388,10 @@ class Contact < ApplicationRecord "#{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 @@ -411,7 +414,7 @@ class Contact < ApplicationRecord # using small rails hack to generate outer join domains = if sorts.first == 'registrar_name'.freeze - domains.includes(:registrar).where.not(registrars: { id: nil }) + domains.where.not(registrars: { id: nil }) .order("registrars.name #{order} NULLS LAST") else domains.order("#{sort} #{order} NULLS LAST") @@ -428,7 +431,6 @@ class Contact < ApplicationRecord end domains.each { |d| d.roles = domain_c[d.id].uniq } - domains end @@ -444,18 +446,28 @@ class Contact < ApplicationRecord end end - def qualified_domain_ids(domain_filter) - registrant_ids = registrant_domains.pluck(:id) - return registrant_ids if domain_filter == 'Registrant' + 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 == '' - 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 + # 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, @@ -465,7 +477,7 @@ class Contact < ApplicationRecord PENDING_CREATE, PENDING_TRANSFER, PENDING_UPDATE, - PENDING_DELETE + PENDING_DELETE, ]).present? end @@ -596,4 +608,14 @@ class Contact < ApplicationRecord 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 diff --git a/app/models/contact_update_action.rb b/app/models/contact_update_action.rb new file mode 100644 index 000000000..4e7444948 --- /dev/null +++ b/app/models/contact_update_action.rb @@ -0,0 +1 @@ +class ContactUpdateAction < Action; end diff --git a/app/models/deposit.rb b/app/models/deposit.rb index 5943f1540..711d59d72 100644 --- a/app/models/deposit.rb +++ b/app/models/deposit.rb @@ -33,6 +33,7 @@ class Deposit def issue_prepayment_invoice return unless valid? + registrar.issue_prepayment_invoice(amount, description) end end diff --git a/app/models/depp/user.rb b/app/models/depp/user.rb index 60c6f6c3d..36fc48fdc 100644 --- a/app/models/depp/user.rb +++ b/app/models/depp/user.rb @@ -31,8 +31,8 @@ module Depp def request(xml) Nokogiri::XML(server.request(xml)).remove_namespaces! - rescue EppErrorResponse => e - Nokogiri::XML(e.response_xml.to_s).remove_namespaces! + rescue EppErrorResponse => e + Nokogiri::XML(e.response_xml.to_s).remove_namespaces! end private diff --git a/app/models/dns/domain_name.rb b/app/models/dns/domain_name.rb index bceb4433b..6c68c3797 100644 --- a/app/models/dns/domain_name.rb +++ b/app/models/dns/domain_name.rb @@ -35,6 +35,7 @@ module DNS def sell_at_auction auction = Auction.new auction.domain = name + auction.platform = 'auto' auction.start ToStdout.msg "Created the auction: #{auction.inspect}" update_whois_from_auction(auction) diff --git a/app/models/domain.rb b/app/models/domain.rb index 7afd8046e..a8fc323a0 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -161,14 +161,6 @@ class Domain < ApplicationRecord attribute: 'hostname' } - validates :tech_domain_contacts, uniqueness_multi: { - attribute: 'contact_code_cache' - } - - validates :admin_domain_contacts, uniqueness_multi: { - attribute: 'contact_code_cache' - } - validates :dnskeys, uniqueness_multi: { attribute: 'public_key' } @@ -726,7 +718,6 @@ class Domain < ApplicationRecord hash = super hash['auth_info'] = hash.delete('transfer_code') # API v1 requirement hash['valid_from'] = hash['created_at'] # API v1 requirement - hash.delete('statuses_before_force_delete') hash end diff --git a/app/models/domain_contact.rb b/app/models/domain_contact.rb index 910f4e445..4a791493f 100644 --- a/app/models/domain_contact.rb +++ b/app/models/domain_contact.rb @@ -6,32 +6,21 @@ class DomainContact < ApplicationRecord belongs_to :contact belongs_to :domain + validates :contact, presence: true + + after_destroy :update_contact attr_accessor :value_typeahead + attr_writer :contact_code self.ignored_columns = %w[legacy_domain_id legacy_contact_id] - def epp_code_map - { - '2302' => [ - [:contact_code_cache, :taken, { value: { obj: 'contact', val: contact_code_cache } }] - ] - } - end - def name return 'Tech' if type == 'TechDomainContact' return 'Admin' if type == 'AdminDomainContact' + '' end - validates :contact, presence: true - - before_save :update_contact_code_cache - def update_contact_code_cache - self.contact_code_cache = contact.code - end - - after_destroy :update_contact def update_contact Contact.find(contact_id).save end diff --git a/app/models/domain_transfer.rb b/app/models/domain_transfer.rb index 02ab2bc88..ff9e55276 100644 --- a/app/models/domain_transfer.rb +++ b/app/models/domain_transfer.rb @@ -4,10 +4,10 @@ class DomainTransfer < ApplicationRecord belongs_to :old_registrar, class_name: 'Registrar' belongs_to :new_registrar, class_name: 'Registrar' - PENDING = 'pending' - CLIENT_APPROVED = 'clientApproved' - CLIENT_REJECTED = 'clientRejected' - SERVER_APPROVED = 'serverApproved' + PENDING = 'pending'.freeze + CLIENT_APPROVED = 'clientApproved'.freeze + CLIENT_REJECTED = 'clientRejected'.freeze + SERVER_APPROVED = 'serverApproved'.freeze before_create :set_wait_until diff --git a/app/models/feature.rb b/app/models/feature.rb index 4143f108e..7a0d6d78b 100644 --- a/app/models/feature.rb +++ b/app/models/feature.rb @@ -1,13 +1,7 @@ class Feature - # def self.obj_and_extensions_statuses_enabled? - # return false if ENV['obj_and_extensions_prohibited'] == 'false' - # - # ENV['obj_and_extensions_prohibited'] || false - # end - # - # def self.enable_lock_domain_with_new_statuses? - # return false if ENV['enable_lock_domain_with_new_statuses'] == 'false' - # - # ENV['enable_lock_domain_with_new_statuses'] || false - # end + def self.billing_system_integrated? + return false if ENV['billing_system_integrated'] == 'false' + + ENV['billing_system_integrated'] || false + end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 6ba3a158d..80d6cf5d2 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -32,30 +32,43 @@ class Invoice < ApplicationRecord # rubocop:enable Layout/LineLength # rubocop:enable Style/MultilineBlockLayout validates :due_date, :currency, :seller_name, - :seller_iban, :buyer_name, :items, presence: true + :seller_iban, :buyer_name, presence: true + validates :items, presence: true, unless: -> { monthly_invoice } before_create :set_invoice_number before_create :calculate_total, unless: :total? before_create :apply_default_buyer_vat_no, unless: :buyer_vat_no? + skip_callback :create, :before, :set_invoice_number, if: -> { monthly_invoice } + skip_callback :create, :before, :calculate_total, if: -> { monthly_invoice } attribute :vat_rate, ::Type::VatRate.new - def set_invoice_number - last_no = Invoice.order(number: :desc).limit(1).pick(:number) + def validate_invoice_number(result) + response = JSON.parse(result.body) - if last_no && last_no >= Setting.invoice_number_min.to_i - self.number = last_no + 1 - else - self.number = Setting.invoice_number_min.to_i - end + billing_restrictions_issue if response['code'] == '403' + billing_out_of_range_issue if response['error'] == 'out of range' + end - return if number <= Setting.invoice_number_max.to_i + def billing_restrictions_issue + errors.add(:base, I18n.t('cannot get access')) + logger.error('PROBLEM WITH TOKEN') + throw(:abort) + end + def billing_out_of_range_issue errors.add(:base, I18n.t('failed_to_generate_invoice_invoice_number_limit_reached')) logger.error('INVOICE NUMBER LIMIT REACHED, COULD NOT GENERATE INVOICE') throw(:abort) end + def set_invoice_number + result = EisBilling::GetInvoiceNumber.send_invoice + validate_invoice_number(result) + + self.number = JSON.parse(result.body)['invoice_number'].to_i + end + def to_s I18n.t('invoice_no', no: number) end @@ -82,7 +95,7 @@ class Invoice < ApplicationRecord end def subtotal - items.map(&:item_sum_without_vat).reduce(:+) + items.map(&:item_sum_without_vat).reduce(:+) || 0 end def vat_amount @@ -95,7 +108,11 @@ class Invoice < ApplicationRecord end def each(&block) - items.each(&block) + if monthly_invoice + metadata['items'].map { |el| OpenStruct.new(el) }.each(&block) + else + items.each(&block) + end end def as_pdf @@ -144,6 +161,13 @@ class Invoice < ApplicationRecord private + ransacker :number_str do + Arel.sql( + "regexp_replace( + to_char(\"#{table_name}\".\"number\", '999999999999'), ' ', '', 'g')" + ) + end + def receipt_date_status if paid? receipt_date diff --git a/app/models/invoice/e_invoice_generator.rb b/app/models/invoice/e_invoice_generator.rb index 2361656a7..8d9675475 100644 --- a/app/models/invoice/e_invoice_generator.rb +++ b/app/models/invoice/e_invoice_generator.rb @@ -41,22 +41,16 @@ class Invoice e_invoice_invoice_items = [] invoice.each do |invoice_item| - e_invoice_invoice_item = EInvoice::InvoiceItem.new.tap do |i| - i.description = invoice_item.description - i.price = invoice_item.price - i.quantity = invoice_item.quantity - i.unit = invoice_item.unit - i.subtotal = invoice_item.subtotal - i.vat_rate = invoice_item.vat_rate - i.vat_amount = invoice_item.vat_amount - i.total = invoice_item.total - end + e_invoice_invoice_item = generate_invoice_item(invoice, invoice_item) e_invoice_invoice_items << e_invoice_invoice_item end + e_invoice_name_item = e_invoice_invoice_items.shift if invoice.monthly_invoice + e_invoice_invoice = EInvoice::Invoice.new.tap do |i| i.seller = seller i.buyer = buyer + i.name = e_invoice_name_item&.description i.items = e_invoice_invoice_items i.number = invoice.number i.date = invoice.issue_date @@ -72,9 +66,33 @@ class Invoice i.currency = invoice.currency i.delivery_channel = %i[internet_bank portal] i.payable = payable + i.monthly_invoice = invoice.monthly_invoice end EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice) end + + private + + def generate_invoice_item(invoice, item) + EInvoice::InvoiceItem.new.tap do |i| + i.description = item.description + i.unit = item.unit + i.price = item.price + i.quantity = item.quantity + if invoice.monthly_invoice && item.price && item.quantity + i.product_id = item.product_id + i.vat_rate = invoice.vat_rate + i.subtotal = (item.price * item.quantity).round(3) + i.vat_amount = i.subtotal * (i.vat_rate / 100) + i.total = i.subtotal + i.vat_amount + else + i.subtotal = item.subtotal + i.vat_rate = item.vat_rate + i.vat_amount = item.vat_amount + i.total = item.total + end + end + end end end diff --git a/app/models/invoice/pdf_generator.rb b/app/models/invoice/pdf_generator.rb index 14fb99814..7762456c4 100644 --- a/app/models/invoice/pdf_generator.rb +++ b/app/models/invoice/pdf_generator.rb @@ -14,7 +14,8 @@ class Invoice private def invoice_html - ApplicationController.render(template: 'invoice/pdf', assigns: { invoice: invoice }) + template = invoice.monthly_invoice ? 'invoice/monthly_pdf' : 'invoice/pdf' + ApplicationController.render(template: template, assigns: { invoice: invoice }) end end end diff --git a/app/models/notification.rb b/app/models/notification.rb index c9af66c56..8cb4335a2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,5 +1,6 @@ class Notification < ApplicationRecord include Versions # version/notification_version.rb + include EppErrors belongs_to :registrar belongs_to :action, optional: true diff --git a/app/models/registrar.rb b/app/models/registrar.rb index d7ba62306..07d7d4795 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -1,4 +1,4 @@ -class Registrar < ApplicationRecord +class Registrar < ApplicationRecord # rubocop:disable Metrics/ClassLength include Versions # version/registrar_version.rb include Registrar::BookKeeping include EmailVerifable @@ -34,12 +34,12 @@ class Registrar < ApplicationRecord attribute :vat_rate, ::Type::VatRate.new after_initialize :set_defaults - validate :correct_email_format, if: proc { |c| c.will_save_change_to_email? } - validate :correct_billing_email_format + # validate :correct_email_format, if: proc { |c| c.will_save_change_to_email? } + # validate :correct_billing_email_format alias_attribute :contact_email, :email - WHOIS_TRIGGERS = %w(name email phone street city state zip) + WHOIS_TRIGGERS = %w[name email phone street city state zip].freeze after_commit :update_whois_records def update_whois_records @@ -56,9 +56,48 @@ class Registrar < ApplicationRecord end end - def issue_prepayment_invoice(amount, description = nil, payable: true) - vat_rate = ::Invoice::VatRateCalculator.new(registrar: self).calculate + # rubocop:disable Metrics/MethodLength + def init_monthly_invoice(summary) + Invoice.new( + issue_date: summary['date'].to_date, + due_date: summary['date'].to_date, + currency: 'EUR', + description: I18n.t('invoice.monthly_invoice_description'), + seller_name: Setting.registry_juridical_name, + seller_reg_no: Setting.registry_reg_no, + seller_iban: Setting.registry_iban, + seller_bank: Setting.registry_bank, + seller_swift: Setting.registry_swift, + seller_vat_no: Setting.registry_vat_no, + seller_country_code: Setting.registry_country_code, + seller_state: Setting.registry_state, + seller_street: Setting.registry_street, + seller_city: Setting.registry_city, + seller_zip: Setting.registry_zip, + seller_phone: Setting.registry_phone, + seller_url: Setting.registry_url, + seller_email: Setting.registry_email, + seller_contact_name: Setting.registry_invoice_contact, + buyer: self, + buyer_name: name, + buyer_reg_no: reg_no, + buyer_country_code: address_country_code, + buyer_state: address_state, + buyer_street: address_street, + buyer_city: address_city, + buyer_zip: address_zip, + buyer_phone: phone, + buyer_url: website, + buyer_email: email, + reference_no: reference_no, + vat_rate: calculate_vat_rate, + monthly_invoice: true, + metadata: { items: summary['invoice_lines'] }, + total: 0 + ) + end + def issue_prepayment_invoice(amount, description = nil, payable: true) invoice = invoices.create!( issue_date: Time.zone.today, due_date: (Time.zone.now + Setting.days_to_keep_invoices_active.days).to_date, @@ -91,14 +130,14 @@ class Registrar < ApplicationRecord buyer_url: website, buyer_email: email, reference_no: reference_no, - vat_rate: vat_rate, + vat_rate: calculate_vat_rate, items_attributes: [ { description: 'prepayment', unit: 'piece', quantity: 1, - price: amount - } + price: amount, + }, ] ) @@ -107,10 +146,17 @@ class Registrar < ApplicationRecord .deliver_later(wait: 1.minute) end + add_invoice_instance = EisBilling::AddDeposits.new(invoice) + result = add_invoice_instance.send_invoice + + link = JSON.parse(result.body)['everypay_link'] + + invoice.update(payment_link: link) SendEInvoiceJob.set(wait: 1.minute).perform_now(invoice.id, payable: payable) invoice end + # rubocop:enable Metrics/MethodLength def cash_account accounts.find_by(account_type: Account::CASH) @@ -175,9 +221,9 @@ class Registrar < ApplicationRecord end def add_nameservers(new_attributes, domains: []) - transaction do - return if domains.empty? + return [] if domains.empty? + transaction do approved_list = domain_list_processing(domains: domains, new_attributes: new_attributes) self.domains.where(name: approved_list).find_each(&:update_whois_record) if approved_list.any? @@ -220,13 +266,9 @@ class Registrar < ApplicationRecord def notify(action) text = I18n.t("notifications.texts.#{action.notification_key}", contact: action.contact&.code, count: action.subactions&.count) - if action.bulk_action? - notifications.create!(text: text, action_id: action.id, - attached_obj_type: 'BulkAction', - attached_obj_id: action.id) - else - notifications.create!(text: text) - end + notifications.create!(text: text, action_id: action.id, + attached_obj_type: 'ContactUpdateAction', + attached_obj_id: action.id) end def e_invoice_iban @@ -256,4 +298,8 @@ class Registrar < ApplicationRecord def vat_liable_in_foreign_country? !vat_liable_locally? end + + def calculate_vat_rate + ::Invoice::VatRateCalculator.new(registrar: self).calculate + end end diff --git a/app/models/tech_domain_contact.rb b/app/models/tech_domain_contact.rb index eff815350..7c3d22bfd 100644 --- a/app/models/tech_domain_contact.rb +++ b/app/models/tech_domain_contact.rb @@ -5,7 +5,7 @@ class TechDomainContact < DomainContact skipped_domains = [] tech_contacts = where(contact: current_contact) - tech_contacts.each do |tech_contact| + tech_contacts.includes(:domain).find_each do |tech_contact| if irreplaceable?(tech_contact) skipped_domains << tech_contact.domain.name next diff --git a/app/models/user.rb b/app/models/user.rb index cca07ca14..8ee0ea05c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,9 +13,9 @@ class User < ApplicationRecord def self.from_omniauth(omniauth_hash) uid = omniauth_hash['uid'] - identity_code = uid.slice(2..-1) + identity_code = uid&.slice(2..-1) # country_code = uid.slice(0..1) - find_by(identity_code: identity_code) + find_by(identity_code: identity_code, active: true) end end diff --git a/app/models/validation_event.rb b/app/models/validation_event.rb index 49bf4325a..3e7670fac 100644 --- a/app/models/validation_event.rb +++ b/app/models/validation_event.rb @@ -27,7 +27,7 @@ class ValidationEvent < ApplicationRecord belongs_to :validation_eventable, polymorphic: true - scope :recent, -> { where('created_at < ?', Time.zone.now - VALIDATION_PERIOD) } + scope :old_records, -> { where('created_at < ?', Time.zone.now - VALIDATION_PERIOD) } scope :successful, -> { where(success: true) } scope :failed, -> { where(success: false) } scope :regex, -> { where('event_data @> ?', { 'check_level': 'regex' }.to_json) } @@ -35,11 +35,11 @@ class ValidationEvent < ApplicationRecord scope :smtp, -> { where('event_data @> ?', { 'check_level': 'smtp' }.to_json) } scope :by_object, ->(object) { where(validation_eventable: object) } - after_create :check_for_force_delete - def self.validated_ids_by(klass) - recent.successful.where('validation_eventable_type = ?', klass) - .pluck(:validation_eventable_id) + old_records + .successful + .where('validation_eventable_type = ?', klass) + .pluck(:validation_eventable_id) end def failed? @@ -57,56 +57,4 @@ class ValidationEvent < ApplicationRecord def object validation_eventable end - - def check_for_force_delete - if object.need_to_start_force_delete? - start_force_delete - elsif object.need_to_lift_force_delete? - refresh_status_notes - lift_force_delete - end - end - - def start_force_delete - Domains::ForceDeleteEmail::Base.run(email: email) - end - - def refresh_status_notes - domain_list.each do |domain| - next unless domain.status_notes[DomainStatus::FORCE_DELETE] - - domain.status_notes[DomainStatus::FORCE_DELETE].slice!(object.email_history) - domain.status_notes[DomainStatus::FORCE_DELETE].lstrip! - domain.save(validate: false) - - notify_registrar(domain) unless domain.status_notes[DomainStatus::FORCE_DELETE].empty? - end - end - - def domain_list - domain_contacts = Contact.where(email: email).map(&:domain_contacts).flatten - registrant_ids = Registrant.where(email: email).pluck(:id) - - (domain_contacts.map(&:domain).flatten + Domain.where(registrant_id: registrant_ids)).uniq - end - - def notify_registrar(domain) - domain.registrar.notifications.create!(text: I18n.t('force_delete_auto_email', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date, - email: domain.status_notes[DomainStatus::FORCE_DELETE])) - end - - def lift_force_delete - # domain_contacts = Contact.where(email: email).map(&:domain_contacts).flatten - # registrant_ids = Registrant.where(email: email).pluck(:id) - # - # domains = domain_contacts.map(&:domain).flatten + - # Domain.where(registrant_id: registrant_ids) - # - # domains.each do |domain| - # Domains::ForceDeleteLift::Base.run(domain: domain) - # end - end end diff --git a/app/presenters/registrar/domain_list_csv_presenter.rb b/app/presenters/registrar/domain_list_csv_presenter.rb index e38f3f54e..a216d9561 100644 --- a/app/presenters/registrar/domain_list_csv_presenter.rb +++ b/app/presenters/registrar/domain_list_csv_presenter.rb @@ -17,13 +17,13 @@ class Registrar::DomainListCsvPresenter private def header - columns = %w( + columns = %w[ domain_name transfer_code registrant_name registrant_code expire_time - ) + ] columns.map! { |column| view.t("registrar.domains.index.csv.#{column}") } @@ -37,7 +37,6 @@ class Registrar::DomainListCsvPresenter row[2] = domain.registrant.name row[3] = domain.registrant.code row[4] = domain.expire_date - row CSV::Row.new([], row) end diff --git a/app/services/eis_billing/add_deposits.rb b/app/services/eis_billing/add_deposits.rb new file mode 100644 index 000000000..3ccd2db88 --- /dev/null +++ b/app/services/eis_billing/add_deposits.rb @@ -0,0 +1,38 @@ +module EisBilling + class AddDeposits < EisBilling::Base + attr_reader :invoice + + def initialize(invoice) + @invoice = invoice + end + + def send_invoice + send_request(json_obj: parse_invoice) + end + + private + + def parse_invoice + data = {} + data[:transaction_amount] = invoice.total.to_s + data[:order_reference] = invoice.number + data[:customer_name] = invoice.buyer_name + data[:customer_email] = invoice.buyer_email + data[:custom_field1] = invoice.description + data[:custom_field2] = INITIATOR + data[:invoice_number] = invoice.number + data[:reference_number] = invoice.reference_no + + data + end + + def send_request(json_obj:) + http = EisBilling::Base.base_request(url: invoice_generator_url) + http.post(invoice_generator_url, json_obj.to_json, EisBilling::Base.headers) + end + + def invoice_generator_url + "#{BASE_URL}/api/v1/invoice_generator/invoice_generator" + end + end +end diff --git a/app/services/eis_billing/base.rb b/app/services/eis_billing/base.rb new file mode 100644 index 000000000..e0cb71b4a --- /dev/null +++ b/app/services/eis_billing/base.rb @@ -0,0 +1,35 @@ +module EisBilling + class Base + BASE_URL = ENV['eis_billing_system_base_url'] || 'https://st-billing.infra.tld.ee' + INITIATOR = 'registry'.freeze + + def self.base_request(url:) + uri = URI(url) + http = Net::HTTP.new(uri.host, uri.port) + + http.use_ssl = true unless Rails.env.development? + http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Rails.env.development? + + http + end + + def self.generate_token + JWT.encode(payload, billing_secret) + end + + def self.payload + { initiator: INITIATOR } + end + + def self.headers + { + 'Authorization' => "Bearer #{generate_token}", + 'Content-Type' => 'application/json', + } + end + + def self.billing_secret + ENV['billing_secret'] + end + end +end diff --git a/app/services/eis_billing/get_invoice_number.rb b/app/services/eis_billing/get_invoice_number.rb new file mode 100644 index 000000000..1ef31eeb0 --- /dev/null +++ b/app/services/eis_billing/get_invoice_number.rb @@ -0,0 +1,16 @@ +module EisBilling + class GetInvoiceNumber < EisBilling::Base + def self.send_invoice + send_request + end + + def self.send_request + http = EisBilling::Base.base_request(url: invoice_number_generator_url) + http.post(invoice_number_generator_url, nil, EisBilling::Base.headers) + end + + def self.invoice_number_generator_url + "#{BASE_URL}/api/v1/invoice_generator/invoice_number_generator" + end + end +end diff --git a/app/services/eis_billing/get_reference_number.rb b/app/services/eis_billing/get_reference_number.rb new file mode 100644 index 000000000..e200a8dff --- /dev/null +++ b/app/services/eis_billing/get_reference_number.rb @@ -0,0 +1,22 @@ +module EisBilling + class GetReferenceNumber < EisBilling::Base + def self.send_request + send_it + end + + def self.obj_data + { + initiator: INITIATOR, + } + end + + def self.send_it + http = EisBilling::Base.base_request(url: reference_number_generator_url) + http.post(reference_number_generator_url, obj_data.to_json, EisBilling::Base.headers) + end + + def self.reference_number_generator_url + "#{EisBilling::Base::BASE_URL}/api/v1/invoice_generator/reference_number_generator" + end + end +end diff --git a/app/services/eis_billing/send_data_to_directo.rb b/app/services/eis_billing/send_data_to_directo.rb new file mode 100644 index 000000000..266475b31 --- /dev/null +++ b/app/services/eis_billing/send_data_to_directo.rb @@ -0,0 +1,23 @@ +module EisBilling + class SendDataToDirecto < EisBilling::Base + def self.send_request(object_data:, monthly:, dry:) + send_info(object_data: object_data, monthly: monthly, dry: dry) + end + + def self.send_info(object_data:, monthly:, dry:) + prepared_data = { + invoice_data: object_data, + monthly: monthly, + dry: dry, + initiator: INITIATOR, + } + + http = EisBilling::Base.base_request(url: directo_url) + http.post(directo_url, prepared_data.to_json, EisBilling::Base.headers) + end + + def self.directo_url + "#{BASE_URL}/api/v1/directo/directo" + end + end +end diff --git a/app/services/eis_billing/send_e_invoice.rb b/app/services/eis_billing/send_e_invoice.rb new file mode 100644 index 000000000..e2a9cfaf6 --- /dev/null +++ b/app/services/eis_billing/send_e_invoice.rb @@ -0,0 +1,52 @@ +module EisBilling + class SendEInvoice < EisBilling::Base + def self.send_request(invoice:, payable:) + send_info(invoice: invoice, payable: payable) + end + + def self.send_info(invoice:, payable:) + items = [] + prepared_data = prepare_data(invoice: invoice, payable: payable) + + invoice.items.each do |invoice_item| + items << prepare_item(invoice_item) + end + + prepared_data[:items] = items + + http = EisBilling::Base.base_request(url: e_invoice_url) + http.post(e_invoice_url, prepared_data.to_json, EisBilling::Base.headers) + end + + def self.prepare_item(invoice_item) + { + description: invoice_item.description, + price: invoice_item.price, + quantity: invoice_item.quantity, + unit: invoice_item.unit, + subtotal: invoice_item.subtotal, + vat_rate: invoice_item.vat_rate, + vat_amount: invoice_item.vat_amount, + total: invoice_item.total, + } + end + + def self.prepare_data(invoice:, payable:) + { + invoice: invoice, + vat_amount: invoice.vat_amount, + invoice_subtotal: invoice.subtotal, + buyer_billing_email: invoice.buyer.billing_email, + buyer_e_invoice_iban: invoice.buyer.e_invoice_iban, + seller_country_code: invoice.seller_country_code, + buyer_country_code: invoice.buyer_country_code, + payable: payable, + initiator: EisBilling::Base::INITIATOR, + } + end + + def self.e_invoice_url + "#{BASE_URL}/api/v1/e_invoice/e_invoice" + end + end +end diff --git a/app/services/eis_billing/send_invoice_status.rb b/app/services/eis_billing/send_invoice_status.rb new file mode 100644 index 000000000..61007675e --- /dev/null +++ b/app/services/eis_billing/send_invoice_status.rb @@ -0,0 +1,21 @@ +module EisBilling + class SendInvoiceStatus < EisBilling::Base + def self.send_info(invoice_number:, status:) + send_request(invoice_number: invoice_number, status: status) + end + + def self.send_request(invoice_number:, status:) + json_obj = { + invoice_number: invoice_number, + status: status, + } + + http = EisBilling::Base.base_request(url: invoice_status_url) + http.post(invoice_status_url, json_obj.to_json, EisBilling::Base.headers) + end + + def self.invoice_status_url + "#{BASE_URL}/api/v1/invoice_generator/invoice_status" + end + end +end diff --git a/app/services/nameserver_validator.rb b/app/services/nameserver_validator.rb index c45003cd3..6436ba363 100644 --- a/app/services/nameserver_validator.rb +++ b/app/services/nameserver_validator.rb @@ -35,8 +35,14 @@ module NameserverValidator return { result: false, reason: 'answer' } if result.answer.empty? + decision = result.answer.any? do |a| + a.type == 'CNAME' + end + + return { result: false, reason: 'cname' } if decision + decision = result.answer.all? do |a| - a.serial.present? + a.instance_variable_defined? '@serial' end return { result: false, reason: 'serial' } unless decision diff --git a/app/services/partial_search_formatter.rb b/app/services/partial_search_formatter.rb index af0c7978d..268cd75fb 100644 --- a/app/services/partial_search_formatter.rb +++ b/app/services/partial_search_formatter.rb @@ -5,7 +5,7 @@ class PartialSearchFormatter search_params.each do |key, value| next unless key.include?('matches') && value.present? - value << '%' + search_params[key] = "%#{value}%" end search_params diff --git a/app/views/admin/auctions/_modal.html.erb b/app/views/admin/auctions/_modal.html.erb new file mode 100644 index 000000000..e57d2139d --- /dev/null +++ b/app/views/admin/auctions/_modal.html.erb @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/app/views/admin/auctions/index.html.erb b/app/views/admin/auctions/index.html.erb new file mode 100644 index 000000000..85d533fa7 --- /dev/null +++ b/app/views/admin/auctions/index.html.erb @@ -0,0 +1,155 @@ + + +
+
+ <%= form_with url: admin_auctions_path, method: :get, html: { style: 'margin-bottom: 0;', class: 'js-form', autocomplete: 'off' } do |f| %> +
+
+
+ <%= f.label :domain %> + <%= f.search_field :domain_matches, value: params[:domain_matches], class: 'form-control', placeholder: t(:name) %> +
+
+ <%= f.label :status %> + <%= select_tag :statuses_contains, options_for_select(Auction.statuses.map { |x| [x[0], x[1]] }, params[:q][:status]), { include_blank:true, class: 'form-control' } %> +
+
+
+
+ <%= f.label t(:created_at_from) %> + <%= f.search_field :created_at_start, value: params[:created_at_start], class: 'form-control js-datepicker', placeholder: t(:created_at_from) %> +
+
+
+
+ <%= f.label t(:created_at_until) %> + <%= f.search_field :created_at_end, value: params[:created_at_end], class: 'form-control js-datepicker', placeholder: t(:created_at_until) %> +
+
+
+
+ <%= label_tag t(:results_per_page) %> + <%= text_field_tag :results_per_page_auction, params[:results_per_page_auction], class: 'form-control', placeholder: t(:results_per_page) %> +
+
+
+ +
+ <%= link_to('Clear', admin_auctions_path, class: 'btn btn-default') %> +
+
+ <%= link_to 'Download auction list', admin_auctions_path(format: :csv, params: params.permit!), + "data-toggle" => "tooltip", "data-placement" => "bottom", "title" => 'Download CSV', + class: 'btn btn-primary' %> +
+
+ <%= link_to "#", class: "btn btn-warning edit", id: 'reserved-modal', + data: { + toggle: "modal", + url: admin_reserved_domains_path, + target: "#user-form-edit"} do %> + + Get reserved domains + <% end %> + + <%= render 'modal' %> + +
+
+
+ <% end %> +
+
+ +
+ + <%= search_form_for [:admin, @q], method: :post, html: { style: 'margin-bottom: 0;', class: 'js-form', autocomplete: 'off' } do |f| %> + <%= label_tag :new_auction %> + +
+ <%= text_field_tag :domain, params[:domain], class: 'form-control', placeholder: 'domain name' %> + <%= f.submit 'Create', class: 'btn btn-primary', style: 'margin-left: .4rem;', id: 'new-auction-btn' %> +
+ <% end %> + +
+ +
+
+ <%= search_form_for @q, url: upload_spreadsheet_admin_auctions_path, method: :post, html: { style: 'margin-bottom: 0; display: flex; flex-direction: row; align-items: center;', class: 'js-form', autocomplete: 'off' } do |f| %> + <%= f.file_field :file, + accept: ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel", + direct_upload: true, + style: 'width: 200px;' %> + <%= f.submit 'Upload csv', class: 'btn btn-primary' %> + <% end %> + +
+
+ +
+
+
+ + + + + + + + + + + + + + <% @auctions.each do |auction| %> + + + + + + + + + <% end %> + +
+ <%= sort_link(@q, 'domain') %> + + <%= sort_link(@q, 'status') %> + + <%= sort_link(@q, 'created_at') %> + + <%= sort_link(@q, 'registration_code') %> + + <%= sort_link(@q, 'registration_deadline') %> + + <%= sort_link(@q, 'platform', 'Type') %> +
<%= colorize_auction(auction) %><%= auction.status %><%= auction.created_at %><%= auction.registration_code %><%= auction.registration_deadline %><%= auction.platform.nil? ? 'auto' : auction.platform %>
+
+
+
+ +
+
+ <%= paginate @auctions %> +
+
+ +
+
+ + \ No newline at end of file diff --git a/app/views/admin/base/_menu.haml b/app/views/admin/base/_menu.haml index 4e90ccf57..f53fddb0a 100644 --- a/app/views/admin/base/_menu.haml +++ b/app/views/admin/base/_menu.haml @@ -31,6 +31,7 @@ %li.dropdown-header= t(:system) %li= link_to t('.settings'), admin_settings_path %li= link_to t('.zones'), admin_zones_path + %li= link_to t(:auctions), admin_auctions_path %li= link_to t('.blocked_domains'), admin_blocked_domains_path %li= link_to t('.reserved_domains'), admin_reserved_domains_path %li= link_to t('.disputed_domains'), admin_disputes_path diff --git a/app/views/admin/contacts/index.haml b/app/views/admin/contacts/index.haml index 8e7a2c244..b5bfa89f1 100644 --- a/app/views/admin/contacts/index.haml +++ b/app/views/admin/contacts/index.haml @@ -63,10 +63,6 @@ .form-group = label_tag :only_no_country_code, "Ident CC missing" = check_box_tag :only_no_country_code, '1',params[:only_no_country_code].eql?('1'), style: 'width:auto;height:auto;float:right' - .col-md-3 - .form-group - = label_tag :email_verification_failed, "Email verification failed" - = check_box_tag :email_verification_failed, '1',params[:email_verification_failed].eql?('1'), style: 'width:auto;height:auto;float:right' .row .col-md-3{style: 'padding-top: 25px;float:right;padding-right: 0px'} diff --git a/app/views/admin/invoices/_search_form.html.erb b/app/views/admin/invoices/_search_form.html.erb index 7739387b2..91ccf4421 100644 --- a/app/views/admin/invoices/_search_form.html.erb +++ b/app/views/admin/invoices/_search_form.html.erb @@ -12,7 +12,7 @@
<%= f.label t(:status) %> - <%= select_tag :status, options_for_select(%w(Paid Unpaid Cancelled),params[:status]), + <%= select_tag :status, options_for_select(%w(Paid Unpaid Cancelled Monthly),params[:status]), { multiple: false, include_blank: true, selected: params[:status], class: 'form-control selectize'} %>
diff --git a/app/views/admin/invoices/index.haml b/app/views/admin/invoices/index.haml index 046772d68..8b8fe12c3 100644 --- a/app/views/admin/invoices/index.haml +++ b/app/views/admin/invoices/index.haml @@ -31,6 +31,8 @@ %td= l invoice.receipt_date - elsif invoice.cancelled? %td.text-grey= t(:cancelled) + - elsif invoice.monthly_invoice + %td= l invoice.issue_date - else %td.text-danger= t(:unpaid) diff --git a/app/views/admin/invoices/show.haml b/app/views/admin/invoices/show.haml index b121c8337..42f7d769c 100644 --- a/app/views/admin/invoices/show.haml +++ b/app/views/admin/invoices/show.haml @@ -4,11 +4,12 @@ = @invoice .col-sm-8 %h1.text-right.text-center-xs - - if @invoice.unpaid? - = link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default') + - unless @invoice.monthly_invoice + - if @invoice.unpaid? + = link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default') - - if @invoice.paid? and !@invoice.cancelled? - = link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning') + - if @invoice.paid? && !@invoice.cancelled? + = link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning') = link_to(t('.download_btn'), download_admin_invoice_path(@invoice), class: 'btn btn-default') = link_to(t('.deliver_btn'), new_admin_invoice_delivery_path(@invoice), class: 'btn btn-default') @@ -24,6 +25,9 @@ .col-md-6= render 'registrar/invoices/partials/seller' .col-md-6= render 'registrar/invoices/partials/buyer' .row - .col-md-12= render 'registrar/invoices/partials/items' + - if @invoice.monthly_invoice + .col-md-12= render 'registrar/invoices/partials/monthly_invoice_items' + - else + .col-md-12= render 'registrar/invoices/partials/items' .row .col-md-12= render 'registrar/invoices/partials/payment_orders' diff --git a/app/views/admin/reserved_domains/index.html.erb b/app/views/admin/reserved_domains/index.html.erb new file mode 100644 index 000000000..109dbad9a --- /dev/null +++ b/app/views/admin/reserved_domains/index.html.erb @@ -0,0 +1,137 @@ +<% content_for :actions do %> + <%= link_to(t('.new_btn'), new_admin_reserved_domain_path, class: 'btn btn-primary') %> +<% end %> + +<%= render 'shared/title', name: t('.title') %> +
+
+ <%= search_form_for [:admin, @q], html: { style: 'margin-bottom: 0;', class: 'js-form', autocomplete: 'off' } do |f| %> +
+
+
+ <%= f.label :name %> + <%= f.search_field :name_matches, value: params[:q][:name_matches], class: 'form-control', placeholder: t(:name) %> +
+
+
+
+ <%= f.label t(:created_at_from) %> + <%= f.search_field :created_at_gteq, value: params[:q][:created_at_gteq], class: 'form-control js-datepicker', placeholder: t(:created_at_from) %> +
+
+
+
+ <%= f.label t(:created_at_until) %> + <%= f.search_field :created_at_lteq, value: params[:q][:created_at_lteq], class: 'form-control js-datepicker', placeholder: t(:created_at_until) %> +
+
+
+
+
+
+ <%= label_tag t(:results_per_page) %> + <%= text_field_tag :results_per_page, params[:results_per_page], class: 'form-control', placeholder: t(:results_per_page) %> +
+
+
+ + <%= link_to(t('.csv_btn'), admin_reserved_domains_path(format: :csv, params: params.permit!), class: 'btn btn-default') %> + <%= link_to(t('.reset_btn'), admin_reserved_domains_path, class: 'btn btn-default') %> +
+
+ <% end %> +
+
+ +
+ +<%= form_for :reserved_elements, url: release_to_auction_admin_reserved_domains_path, html: { class: 'form-horizontal', autocomplete: 'off' } do |f| %> +
+ <%= f.submit 'Send to the auction list', class: 'btn btn-primary', style: 'margin: 10px 0 20px 0;' %> + Domains will be removed from reserved list! +
+ +
+
+
+ + + + + + + + + + + + + <% @domains.each do |x| %> + + + + + + + + + <% end %> + +
+ <%= check_box_tag :check_all %> + + <%= sort_link(@q, 'name') %> + + <%= sort_link(@q, 'password') %> + + <%= sort_link(@q, 'created_at', t(:created_at)) %> + + <%= sort_link(@q, 'updated_at', t(:updated_at)) %> + + <%= t(:actions) %> +
+ <%= f.check_box :domain_ids, { multiple: true }, x.id, nil %> + + <%= x.name %> + + <%= x.password %> + + <%= l(x.created_at, format: :short) %> + + <%= l(x.updated_at, format: :short) %> + + <%= link_to(t(:edit_pw), edit_admin_reserved_domain_path(id: x.id), class: 'btn btn-primary btn-xs') %> + <%= link_to(t(:delete), delete_admin_reserved_domain_path(id: x.id), data: { confirm: t(:are_you_sure) }, class: 'btn btn-danger btn-xs') %> +
+
+
+
+<% end %> + +
+
+ <%= paginate @domains %> +
+
+ +
+
+ + \ No newline at end of file diff --git a/app/views/admin/reserved_domains/index.haml b/app/views/admin/reserved_domains/index2.haml similarity index 90% rename from app/views/admin/reserved_domains/index.haml rename to app/views/admin/reserved_domains/index2.haml index 5444ba34d..a3b49e0b0 100644 --- a/app/views/admin/reserved_domains/index.haml +++ b/app/views/admin/reserved_domains/index2.haml @@ -30,6 +30,7 @@   = link_to(t('.csv_btn'), admin_reserved_domains_path(format: :csv, params: params.permit!), class: 'btn btn-default') = link_to(t('.reset_btn'), admin_reserved_domains_path, class: 'btn btn-default') + = link_to 'Send to auction',release_to_auction_admin_reserved_domains_path, method: :post, class: 'btn btn-default', style: 'margin-top: 5px;' %hr .row .col-md-12 @@ -37,6 +38,7 @@ %table.table.table-hover.table-bordered.table-condensed %thead %tr + %th{class: 'col-xs-1'} %th{class: 'col-xs-2'} = sort_link(@q, 'name') %th{class: 'col-xs-2'} @@ -50,6 +52,8 @@ %tbody - @domains.each do |x| %tr + %td{class: 'text-center'} + = check_box_tag "reserved_domains[domain_ids][]", x.id, false %td= x.name %td= x.password %td= l(x.created_at, format: :short) diff --git a/app/views/eis_billing/directo_response/update.html.erb b/app/views/eis_billing/directo_response/update.html.erb new file mode 100644 index 000000000..693715b2d --- /dev/null +++ b/app/views/eis_billing/directo_response/update.html.erb @@ -0,0 +1,2 @@ +

EisBilling::DirectoResponse#update

+

Find me in app/views/eis_billing/directo_response/update.html.erb

diff --git a/app/views/eis_billing/e_invoice_response/update.html.erb b/app/views/eis_billing/e_invoice_response/update.html.erb new file mode 100644 index 000000000..32fb7d171 --- /dev/null +++ b/app/views/eis_billing/e_invoice_response/update.html.erb @@ -0,0 +1,2 @@ +

EisBilling::EInvoiceResponse#update

+

Find me in app/views/eis_billing/e_invoice_response/update.html.erb

diff --git a/app/views/eis_billing/update.json.erb b/app/views/eis_billing/update.json.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/epp/poll/poll_req.xml.builder b/app/views/epp/poll/poll_req.xml.builder index 0a916e6ad..373b8194b 100644 --- a/app/views/epp/poll/poll_req.xml.builder +++ b/app/views/epp/poll/poll_req.xml.builder @@ -15,7 +15,7 @@ xml.epp_head do xml.resData do xml << render('epp/domains/partials/transfer', builder: xml, dt: @object) end - when 'BulkAction' + when 'ContactUpdateAction' xml.resData do xml << render( 'epp/contacts/partials/check', diff --git a/app/views/invoice/monthly_pdf.haml b/app/views/invoice/monthly_pdf.haml new file mode 100644 index 000000000..c7e179d03 --- /dev/null +++ b/app/views/invoice/monthly_pdf.haml @@ -0,0 +1,277 @@ +%html{lang: I18n.locale.to_s} + %head + %meta{charset: "utf-8"} + :css + .container { + margin: auto; + font-size: 12px; + } + + .col-md-12 { + + } + + .col-md-6 { + width: 49%; + display: inline-block; + } + + .col-xs-4 { + width: 33%; + } + + .col-xs-2 { + width: 16%; + } + + .col-md-3 { + width: 24%; + display: inline-block; + } + + .left { + float: left; + } + + .left { + padding-right: 5px; + } + + .right { + float: right; + } + + .text-right { + text-align: right; + } + + dt { + float: left; + width: 100px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + line-height: 1.42857; + } + + dd { + margin-left: 120px; + line-height: 1.42857; + } + + table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + } + + th { + text-align: left; + border: 0px; + border-top: 1px solid #DDD; + padding: 6px; + } + + thead th { + border-bottom: 2px solid #DDD; + border-top: 0px; + } + + td { + border-top: 1px solid #DDD; + } + + td { + padding: 6px; + } + + .no-border { + border: 0px; + } + + hr { + height: 1px; + border: 0; + color: #DDD; + background-color: #DDD; + } + + .clear { + clear: both; + } + + .pull-down { + margin-top: 50px; + } + + #header { + position: relative; + min-height: 100px; + } + + img { + width: 106px; + height: 102px; + } + + #header-content { + position: absolute; + bottom: 0; + } + + #footer { + position: absolute; + bottom: 0px; + width: 99%; + } + + h1 { + margin-bottom: 5px; + } + %body + .container + #header.row + .col-sm-6.left + #header-content + %h1 + = @invoice + .col-sm-6.right + %img{src: "#{Rails.root}/public/eis-logo-black-et.png"} + .clear + %hr + .row + .col-md-6.left + %h4 + Details + %hr + %dl.dl-horizontal + %dt= t(:issue_date) + %dd= l @invoice.issue_date + + - if @invoice.cancelled? + %dt= Invoice.human_attribute_name :cancelled_at + %dd= l @invoice.cancelled_at + + %dt= t(:due_date) + - if @invoice.cancelled? + %dd= t(:cancelled) + - else + %dd= l @invoice.due_date + + %dt= t(:issuer) + %dd= @invoice.seller_contact_name + + - if @invoice.description.present? + %dt= t(:description) + %dd=@invoice.description + + %dt= Invoice.human_attribute_name :reference_no + %dd= @invoice.reference_no + + .col-md-6.right + %h4= t(:client) + %hr + %dl.dl-horizontal + %dt= t(:name) + %dd= @invoice.buyer_name + + %dt= t(:reg_no) + %dd= @invoice.buyer_reg_no + + - if @invoice.buyer_address.present? + %dt= Invoice.human_attribute_name :address + %dd= @invoice.buyer_address + + - if @invoice.buyer_country.present? + %dt= t(:country) + %dd= @invoice.buyer_country + + - if @invoice.buyer_phone.present? + %dt= t(:phone) + %dd= @invoice.buyer_phone + + - if @invoice.buyer_url.present? + %dt= t(:url) + %dd= @invoice.buyer_url + + - if @invoice.buyer_email.present? + %dt= t(:email) + %dd= @invoice.buyer_email + + .clear + .row.pull-down + .col-md-12 + .table-responsive + %table.table.table-hover.table-condensed + %thead + %tr + %th{class: 'col-xs-1'}= t(:code) + %th{class: 'col-xs-1'}= InvoiceItem.human_attribute_name :quantity + %th{class: 'col-xs-1'}= t(:unit) + %th{class: 'col-xs-5'}= t(:description) + %th{class: 'col-xs-2'}= t(:price) + %th{class: 'col-xs-2'}= t(:total) + %tbody + - @invoice.each do |invoice_item| + %tr + %td= invoice_item.product_id + %td= invoice_item.quantity + %td= invoice_item.unit + %td= invoice_item.description + - if invoice_item.price && invoice_item.quantity + %td= currency(invoice_item.price) + %td= "#{currency((invoice_item.price * invoice_item.quantity).round(3))} #{@invoice.currency}" + - else + %td= '' + %td= '' + %tfoot + %tr + %th{colspan: 4} + %th= Invoice.human_attribute_name :subtotal + %td= number_to_currency(0) + %tr + %th.no-border{colspan: 4} + %th= "VAT #{number_to_percentage(@invoice.vat_rate, precision: 1)}" + %td= number_to_currency(0) + %tr + %th.no-border{colspan: 4} + %th= t(:total) + %td= number_to_currency(0) + + #footer + %hr + .row + .col-md-3.left + = @invoice.seller_name + %br + = @invoice.seller_address + %br + = @invoice.seller_country + %br + = "#{t('reg_no')} #{@invoice.seller_reg_no}" + %br + = "#{Registrar.human_attribute_name :vat_no} #{@invoice.seller_vat_no}" + + .col-md-3.left + = @invoice.seller_phone + %br + = @invoice.seller_email + %br + = @invoice.seller_url + + .col-md-3.text-right.left + = t(:bank) + %br + = t(:iban) + %br + = t(:swift) + + .col-md-3.left + = @invoice.seller_bank + %br + = @invoice.seller_iban + %br + = @invoice.seller_swift \ No newline at end of file diff --git a/app/views/invoice/pdf.haml b/app/views/invoice/pdf.haml index 9f10acdad..dc3d4370a 100644 --- a/app/views/invoice/pdf.haml +++ b/app/views/invoice/pdf.haml @@ -234,7 +234,7 @@ %td= invoice_item.unit %td= invoice_item.quantity %td= currency(invoice_item.price) - %td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}" + %td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}" %tfoot %tr %th{colspan: 3} diff --git a/app/views/layouts/registrant/application.html.erb b/app/views/layouts/registrant/application.html.erb deleted file mode 100644 index c5290b70f..000000000 --- a/app/views/layouts/registrant/application.html.erb +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - <% if content_for? :head_title %> - <%= yield :head_title %> - <% else %> - - <%= t(:registrant_head_title) %> - - <% end %> - <%= csrf_meta_tags %> - <%= stylesheet_link_tag 'registrant-manifest', media: 'all' %> - <%= favicon_link_tag 'favicon.ico' %> - - - - -
- <%= render 'flash_messages' %> - <%= yield %> -
- - <%= javascript_include_tag 'registrant-manifest', async: true %> - - diff --git a/app/views/layouts/registrar/base.html.erb b/app/views/layouts/registrar/base.html.erb index 5881dd400..3c90db35c 100644 --- a/app/views/layouts/registrar/base.html.erb +++ b/app/views/layouts/registrar/base.html.erb @@ -15,7 +15,7 @@ <%= favicon_link_tag 'favicon.ico' %> -