From 6f94d4ca6bcfd75b95b0318b1acc15d810ebf680 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 30 Dec 2025 14:45:03 +0200 Subject: [PATCH 1/8] Add e-invoice opt-out for PDF invoices Allow registrars who accept e-invoices to opt out of receiving additional PDF invoice emails. This reduces duplicate work when e-invoices are already processed directly by accounting. - Add column to registrars table (default: true) - Check e-invoice recipient status from Estonian Business Registry - Skip PDF email when registrar accepts e-invoices AND opts out - Expose setting via REPP API (GET details / PUT update) Closes #2881 --- .../repp/v1/accounts_controller.rb | 3 +- app/models/contact/company_register.rb | 26 ++++++++- app/models/registrar.rb | 16 +++++- .../20251230104312_accept_pdf_invoices.rb | 5 ++ db/structure.sql | 23 +++++--- .../invoice_email_cancellation_test.rb | 55 +++++++++++++++++++ test/test_helper.rb | 9 +++ 7 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20251230104312_accept_pdf_invoices.rb create mode 100644 test/models/registrar/invoice_email_cancellation_test.rb diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb index 2fe716e9c9..9bc15d3672 100644 --- a/app/controllers/repp/v1/accounts_controller.rb +++ b/app/controllers/repp/v1/accounts_controller.rb @@ -38,6 +38,7 @@ def details api_users: serialized_users(current_user.api_users), white_ips: serialized_ips(registrar.white_ips), balance_auto_reload: type, + accept_pdf_invoices: registrar.accept_pdf_invoices, min_deposit: Setting.minimum_deposit }, roles: ApiUser::ROLES, interfaces: WhiteIp::INTERFACES } @@ -117,7 +118,7 @@ def balance private def account_params - params.require(:account).permit(:billing_email, :iban, :new_user_id) + params.require(:account).permit(:billing_email, :iban, :new_user_id, :accept_pdf_invoices) end def index_params diff --git a/app/models/contact/company_register.rb b/app/models/contact/company_register.rb index abde8706c9..b227dbf881 100644 --- a/app/models/contact/company_register.rb +++ b/app/models/contact/company_register.rb @@ -14,7 +14,7 @@ def return_company_status end def return_company_data - return unless org? + return unless is_contact_estonian_org? company_register.simple_data(registration_number: ident.to_s) rescue CompanyRegister::NotAvailableError @@ -25,7 +25,7 @@ def return_company_data end def return_company_details - return unless org? + return unless is_contact_estonian_org? company_register.company_details(registration_number: ident.to_s) rescue CompanyRegister::SOAPFaultError => e @@ -35,6 +35,28 @@ def return_company_details [] end + def e_invoice_recipients + return unless is_contact_estonian_org? + + company_register.e_invoice_recipients(registration_numbers: ident.to_s) + rescue CompanyRegister::SOAPFaultError => e + Rails.logger.error("SOAP Fault getting company details for #{ident}: #{e.message}") + raise e + rescue CompanyRegister::NotAvailableError + [] + end + + def org_contact_accept_e_invoice? + return unless is_contact_estonian_org? + + result = e_invoice_recipients.first + result.status == 'OK' + end + + def is_contact_estonian_org? + org? && country_code == 'EE' + end + def company_register @company_register ||= CompanyRegister::Client.new end diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 659ec7a370..b114ef6ca3 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -151,7 +151,7 @@ def issue_prepayment_invoice(amount, description = nil, payable: true) ] ) - unless payable + unless payable && (accepts_e_invoices? && !accept_pdf_invoices?) InvoiceMailer.invoice_email(invoice: invoice, recipient: billing_email, paid: !payable) .deliver_later(wait: 1.minute) end @@ -307,6 +307,20 @@ def billing_email self[:billing_email] end + def accepts_e_invoices? + return false unless address_country_code == 'EE' + + result = company_register.e_invoice_recipients(registration_numbers: reg_no).first + result.status == 'OK' + rescue CompanyRegister::NotAvailableError, CompanyRegister::SOAPFaultError => e + Rails.logger.error("Error checking e-invoice status for #{reg_no}: #{e.message}") + false + end + + def company_register + @company_register ||= CompanyRegister::Client.new + end + private def domain_not_updatable?(hostname:, domain:) diff --git a/db/migrate/20251230104312_accept_pdf_invoices.rb b/db/migrate/20251230104312_accept_pdf_invoices.rb new file mode 100644 index 0000000000..ba22f4325e --- /dev/null +++ b/db/migrate/20251230104312_accept_pdf_invoices.rb @@ -0,0 +1,5 @@ +class AcceptPdfInvoices < ActiveRecord::Migration[6.1] + def change + add_column :registrars, :accept_pdf_invoices, :boolean, default: true + end +end diff --git a/db/structure.sql b/db/structure.sql index 9e5cb7ad47..02d366e435 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,3 +1,8 @@ +\restrict manNM7YktJCYjflsdAWd12PkDkR5VB5nDAhRVyBVVHwcDScSXNtcviWOHPpPk1Q + +-- Dumped from database version 13.4 (Debian 13.4-4.pgdg110+1) +-- Dumped by pg_dump version 13.22 (Debian 13.22-0+deb11u1) + SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; @@ -2642,7 +2647,8 @@ CREATE TABLE public.registrars ( settings jsonb DEFAULT '{}'::jsonb NOT NULL, legaldoc_optout boolean DEFAULT false NOT NULL, legaldoc_optout_comment text, - email_history character varying + email_history character varying, + accept_pdf_invoices boolean DEFAULT true ); @@ -5280,6 +5286,8 @@ ALTER TABLE ONLY public.users -- PostgreSQL database dump complete -- +\unrestrict manNM7YktJCYjflsdAWd12PkDkR5VB5nDAhRVyBVVHwcDScSXNtcviWOHPpPk1Q + SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES @@ -5768,10 +5776,13 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230707084741'), ('20230710120154'), ('20230711083811'), +('20240722085530'), +('20240723110208'), ('20240816091049'), ('20240816092636'), ('20240924103554'), ('20241015071505'), +('20241022121525'), ('20241030095636'), ('20241104104620'), ('20241112093540'), @@ -5780,13 +5791,11 @@ INSERT INTO "schema_migrations" (version) VALUES ('20241206085817'), ('20250204094550'), ('20250219102811'), -('20250313122119'), -('20250319104749'), ('20250310133151'), +('20250313122119'), ('20250314133357'), -('20240722085530'), -('20240723110208'), -('20241022121525'), -('20250627084536'); +('20250627084536'), +('20250319104749'), +('20251230104312'); diff --git a/test/models/registrar/invoice_email_cancellation_test.rb b/test/models/registrar/invoice_email_cancellation_test.rb new file mode 100644 index 0000000000..5e3fb30f92 --- /dev/null +++ b/test/models/registrar/invoice_email_cancellation_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' + +class RegistrarInvoiceEmailCancellationTest < ActiveJob::TestCase + setup do + @registrar = registrars(:bestnames) + @registrar.update!(address_country_code: 'EE', reference_no: '1232', vat_rate: 24) + @invoice_params = 100 + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_number_generator"). + to_return(status: 200, body: "{\"invoice_number\":\"123456\"}", headers: {}) + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_generator"). + to_return(status: 200, body: "{\"everypay_link\":\"http://link.test\"}", headers: {}) + + stub_request(:put, "https://registry:3000/eis_billing/e_invoice_response"). + to_return(status: 200, body: "{\"invoice_number\":\"123456\"}, {\"date\":\"#{Time.zone.now}\"}", headers: {}) + + stub_request(:post, "https://eis_billing_system:3000/api/v1/e_invoice/e_invoice"). + to_return(status: 200, body: "", headers: {}) + end + + def test_sends_email_when_registrar_does_not_accept_e_invoices_and_pdf_opt_in_is_true + # Name does not contain 'einvoice', so stub returns 'MR' (not OK) + @registrar.update!(name: 'simple-registrar', accept_pdf_invoices: true) + + assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do + @registrar.issue_prepayment_invoice(@invoice_params) + end + end + + def test_sends_email_when_registrar_does_not_accept_e_invoices_and_pdf_opt_in_is_false + # Name does not contain 'einvoice', so stub returns 'MR' (not OK) + @registrar.update!(name: 'simple-registrar', accept_pdf_invoices: false) + + assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do + @registrar.issue_prepayment_invoice(@invoice_params) + end + end + + def test_skips_email_when_registrar_accepts_e_invoices_and_pdf_opt_in_is_false + @registrar.update!(name: 'einvoice-registrar', accept_pdf_invoices: false) + + assert_enqueued_jobs 0, only: ActionMailer::MailDeliveryJob do + @registrar.issue_prepayment_invoice(@invoice_params) + end + end + + def test_sends_email_when_registrar_accepts_e_invoices_BUT_pdf_opt_in_is_true + @registrar.update!(name: 'einvoice-registrar', accept_pdf_invoices: true) + + assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do + @registrar.issue_prepayment_invoice(@invoice_params) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 90c0170ec8..b80e69846e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -46,6 +46,15 @@ def simple_data(registration_number:) def company_details(registration_number:) [] end + + def e_invoice_recipients(registration_numbers:) + if ::Registrar.exists?(reg_no: registration_numbers) + registrar = ::Registrar.find_by(reg_no: registration_numbers) + status = registrar.name.include?('einvoice') ? 'OK' : 'MR' + return [Struct.new(:status).new(status)] + end + [Struct.new(:status).new('OK')] + end end CompanyRegister::Client = CompanyRegisterClientStub From e2647e8f0add208343ba99f35f3e076e23776d4a Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 30 Dec 2025 15:29:18 +0200 Subject: [PATCH 2/8] fixed tests --- app/models/contact/company_register.rb | 2 +- test/models/contact/company_register_test.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/contact/company_register.rb b/app/models/contact/company_register.rb index b227dbf881..e070cfbb51 100644 --- a/app/models/contact/company_register.rb +++ b/app/models/contact/company_register.rb @@ -54,7 +54,7 @@ def org_contact_accept_e_invoice? end def is_contact_estonian_org? - org? && country_code == 'EE' + org? && ident_country_code == 'EE' end def company_register diff --git a/test/models/contact/company_register_test.rb b/test/models/contact/company_register_test.rb index 7b43d4b6ed..1ef1b22675 100644 --- a/test/models/contact/company_register_test.rb +++ b/test/models/contact/company_register_test.rb @@ -5,6 +5,8 @@ class CompanyRegisterTest < ActiveSupport::TestCase def setup @acme_ltd = contacts(:acme_ltd) + @acme_ltd.update!(ident_country_code: 'EE', ident_type: 'org', ident: '12345678') + @john = contacts(:john) @company_register_stub = CompanyRegister::Client.new end From 66ecb148cb5ae74589fe13894f353e8a2e84b5c0 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 27 Jan 2026 09:49:44 +0200 Subject: [PATCH 3/8] updated dockerfile staging --- Dockerfile.staging | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Dockerfile.staging b/Dockerfile.staging index 1b78aaf093..e1dcacd34a 100644 --- a/Dockerfile.staging +++ b/Dockerfile.staging @@ -57,11 +57,12 @@ RUN gem install bundler && \ # Copy application code COPY . . -# Precompile assets in build stage (Node.js 14 is available here) -# Use dummy SECRET_KEY_BASE for asset precompilation only -ARG SECRET_KEY_BASE=dummy_key_for_assets_precompilation -ENV SECRET_KEY_BASE=${SECRET_KEY_BASE} -RUN RAILS_ENV=staging bundle exec rails assets:precompile && \ +# Copy sample config for asset precompilation (real values come from env at runtime) +RUN cp config/application.yml.sample config/application.yml + +# Precompile assets +RUN RAILS_ENV=staging SECRET_KEY_BASE=dummy_for_assets \ + bundle exec rails assets:precompile && \ echo "Assets precompiled successfully for staging" && \ ls -la public/assets/ | head -20 From a6ca784c8989b511507a9dc0d9823e7caa52c5f3 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 27 Jan 2026 10:17:26 +0200 Subject: [PATCH 4/8] updated application.yml.sample --- config/application.yml.sample | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/application.yml.sample b/config/application.yml.sample index c8e8ecbd01..6b5e0ec97d 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -160,9 +160,9 @@ release_domains_to_auction: 'true' auction_api_allowed_ips: '' # 192.0.2.0, 192.0.2.1 action_mailer_default_protocol: # default: http -action_mailer_default_host: +action_mailer_default_host: 'registry.staging' action_mailer_default_port: # default: no port (80) -action_mailer_default_from: # no-reply@example.com +action_mailer_default_from: 'no-reply@registry.staging' action_mailer_force_delete_from: # `From` header for `DomainDeleteMailer#forced` email lhv_p12_keystore: From 243fad9b0d004e4d8572d98b018d2fbabc37de3b Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 27 Jan 2026 10:44:25 +0200 Subject: [PATCH 5/8] added copy command for copyng example of database conf --- Dockerfile.staging | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.staging b/Dockerfile.staging index e1dcacd34a..b2ee0d1abe 100644 --- a/Dockerfile.staging +++ b/Dockerfile.staging @@ -58,7 +58,8 @@ RUN gem install bundler && \ COPY . . # Copy sample config for asset precompilation (real values come from env at runtime) -RUN cp config/application.yml.sample config/application.yml +RUN cp config/application.yml.sample config/application.yml && \ + cp config/database.yml.sample config/database.ym # Precompile assets RUN RAILS_ENV=staging SECRET_KEY_BASE=dummy_for_assets \ From 09c4df9c0c2d04a6354513b45d90857aeea3434f Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 27 Jan 2026 10:56:58 +0200 Subject: [PATCH 6/8] fixed typo --- Dockerfile.staging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.staging b/Dockerfile.staging index b2ee0d1abe..55f40d7d26 100644 --- a/Dockerfile.staging +++ b/Dockerfile.staging @@ -59,7 +59,7 @@ COPY . . # Copy sample config for asset precompilation (real values come from env at runtime) RUN cp config/application.yml.sample config/application.yml && \ - cp config/database.yml.sample config/database.ym + cp config/database.yml.sample config/database.yml # Precompile assets RUN RAILS_ENV=staging SECRET_KEY_BASE=dummy_for_assets \ From f4ee3a8ebe7583322ab9857b5dff0a536aa6f959 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 27 Jan 2026 11:08:28 +0200 Subject: [PATCH 7/8] updated datanase.yml.sample --- config/database.yml.sample | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/config/database.yml.sample b/config/database.yml.sample index 54c01fa6b6..621c0ac965 100644 --- a/config/database.yml.sample +++ b/config/database.yml.sample @@ -16,8 +16,13 @@ default: &default # staging: - <<: *default - database: registry_staging + primary: + <<: *default + database: registry_staging + primary_replica: + <<: *default + database: registry_staging + replica: true demo: <<: *default From 7ae1253d1f76b3b9fd4d713235b6c743cf756fbb Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 30 Jan 2026 10:52:28 +0200 Subject: [PATCH 8/8] added disclose phone number rake task --- lib/tasks/disclose_phone_numbers.rake | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 lib/tasks/disclose_phone_numbers.rake diff --git a/lib/tasks/disclose_phone_numbers.rake b/lib/tasks/disclose_phone_numbers.rake new file mode 100644 index 0000000000..8ff846a380 --- /dev/null +++ b/lib/tasks/disclose_phone_numbers.rake @@ -0,0 +1,45 @@ +# rake disclose_phone_numbers:disclose + +namespace :disclose_phone_numbers do + desc 'Disclose phone numbers' + task disclose: :environment do + reg_numbers = %w[ + 10597973 + 10890199 + 10096260 + 10784403 + 10641728 + 10762679 + 10557933 + 12659649 + 12176224 + 90010019 + 10960801 + 16406158 + 10510593 + 70000740 + 10838419 + 11099473 + 451394720 + 10647754 + 10176042 + 14127885 + 11163283 + 11685113 + 14281238 + 10098106 + 10577829 + 10234957 + 12345678 + 11072764 + 11100236 + ] + + reg_numbers.each do |reg_no| + registrar = Registrar.find_by(reg_no: reg_no) + registrar.update!(accept_pdf_invoices: false) + + Rails.logger.info("For registrar with name #{registrar.name} and reg no #{registrar.reg_no} set accept_pdf_invoices to false") + end + end +end