From 02fec7571bcdd34d207d1d80bc1453cc5be07bcf Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Mon, 4 Nov 2024 13:54:26 +0200 Subject: [PATCH] feat: add callback URLs for business registry domain reservation Add success and failure callback URLs for business registry domain reservation process: - Add success_business_registry_customer_url and failed_business_registry_customer_url fields to reserve_domain_invoices table - Update ReserveDomainInvoice model to handle new callback URLs - Add URL validation in LongReserveDomainsController - Replace linkpay with oneoff_payment_link for consistency - Update tests to cover new functionality - Add wkhtmltopdf and xvfb setup in Dockerfile This change allows the business registry to specify URLs where users should be redirected after successful or failed domain reservation payments. --- Dockerfile | 20 +++- .../long_reserve_domains_controller.rb | 34 +++++- app/models/reserve_domain_invoice.rb | 21 ++-- app/services/eis_billing/oneoff_service.rb | 39 +++++++ ...allback_urls_to_reserve_domain_invoices.rb | 6 + db/structure.sql | 7 +- .../long_reserve_domains_controller_test.rb | 105 +++++++++++++++++- .../reserve_controller_test.rb | 89 --------------- test/models/reserve_domain_invoice_test.rb | 46 +++++++- .../eis_billing/oneoff_service_test.rb | 97 ++++++++++++++++ 10 files changed, 354 insertions(+), 110 deletions(-) create mode 100644 app/services/eis_billing/oneoff_service.rb create mode 100644 db/migrate/20241104104620_add_callback_urls_to_reserve_domain_invoices.rb delete mode 100644 test/integration/api/business_registry/reserve_controller_test.rb create mode 100644 test/services/eis_billing/oneoff_service_test.rb diff --git a/Dockerfile b/Dockerfile index a91f188ff7..e170c443c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,11 @@ RUN apt-get install -y --no-install-recommends > /dev/null \ libxslt1-dev \ libxml2-dev \ python-dev \ - unzip \ + unzip \ +# libc6-i386 \ +# lib32gcc-s1 \ + wkhtmltopdf \ + xvfb \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -81,4 +85,18 @@ RUN gem install bundler && bundle install --jobs 20 --retry 5 ENV PATH="/opt/chrome-linux64:${PATH}" +# RUN apt-get update && apt-get install -y --no-install-recommends > /dev/null \ +# libc6-i386 \ +# lib32gcc-s1 \ +# wkhtmltopdf \ +# xvfb \ +# && apt-get clean \ +# && rm -rf /var/lib/apt/lists/* + +RUN ln -s /lib/ld-linux.so.2 /lib/ld-linux.so.2 || true + +# Обертка для wkhtmltopdf с xvfb +RUN echo '#!/bin/bash\nxvfb-run -a --server-args="-screen 0, 1024x768x24" /usr/bin/wkhtmltopdf "$@"' > /usr/local/bin/wkhtmltopdf \ + && chmod +x /usr/local/bin/wkhtmltopdf + EXPOSE 3000 \ No newline at end of file diff --git a/app/controllers/api/v1/business_registry/long_reserve_domains_controller.rb b/app/controllers/api/v1/business_registry/long_reserve_domains_controller.rb index eca2600e04..9dc5b5bb6c 100644 --- a/app/controllers/api/v1/business_registry/long_reserve_domains_controller.rb +++ b/app/controllers/api/v1/business_registry/long_reserve_domains_controller.rb @@ -9,12 +9,12 @@ class LongReserveDomainsController < BaseController before_action :available_domains?, only: [:create] def create - result = ReserveDomainInvoice.create_list_of_domains(@domain_names) + result = ReserveDomainInvoice.create_list_of_domains(@domain_names, success_business_registry_customer_url, failed_business_registry_customer_url) if result.status_code_success render_success({ message: "Domains are in pending status. Need to pay for domains.", - linkpay: result.linkpay, + oneoff_payment_link: result.oneoff_payment_link, invoice_number: result.invoice_number, available_domains: ReserveDomainInvoice.filter_available_domains(@domain_names) }, :created) @@ -25,6 +25,14 @@ def create private + def success_business_registry_customer_url + params[:success_business_registry_customer_url] + end + + def failed_business_registry_customer_url + params[:failed_business_registry_customer_url] + end + def domain_names @domain_names ||= params[:domain_names] end @@ -52,6 +60,28 @@ def validate_params render_error("Invalid parameter: domain_names must be a non-empty array of valid domain names", :bad_request) return end + + # Валидация URL параметров + if params[:success_business_registry_customer_url].present? + unless valid_url?(params[:success_business_registry_customer_url]) + render_error("Invalid success URL format", :bad_request) + return + end + end + + if params[:failed_business_registry_customer_url].present? + unless valid_url?(params[:failed_business_registry_customer_url]) + render_error("Invalid failed URL format", :bad_request) + return + end + end + end + + def valid_url?(url) + uri = URI.parse(url) + uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + rescue URI::InvalidURIError + false end end end diff --git a/app/models/reserve_domain_invoice.rb b/app/models/reserve_domain_invoice.rb index 27318ce120..73c455763d 100644 --- a/app/models/reserve_domain_invoice.rb +++ b/app/models/reserve_domain_invoice.rb @@ -6,7 +6,7 @@ class InvoiceStruct < Struct.new(:total, :number, :buyer_name, :buyer_email, keyword_init: true) end - class InvoiceResponseStruct < Struct.new(:status_code_success, :linkpay, :invoice_number, :details, + class InvoiceResponseStruct < Struct.new(:status_code_success, :oneoff_payment_link, :invoice_number, :details, keyword_init: true) end @@ -14,9 +14,10 @@ class InvoiceResponseStruct < Struct.new(:status_code_success, :linkpay, :invoic HTTP_OK = '200' HTTP_CREATED = '201' DEFAULT_AMOUNT = '10.00' + ONE_OFF_CUSTOMER_URL = 'https://registry.test' class << self - def create_list_of_domains(domain_names) + def create_list_of_domains(domain_names, success_business_registry_customer_url, failed_business_registry_customer_url) normalized_names = normalize_domain_names(domain_names) available_names = filter_available_domains(normalized_names) @@ -24,9 +25,13 @@ def create_list_of_domains(domain_names) invoice = create_invoice_with_domains(available_names) result = process_invoice(invoice) + oneoff_result = EisBilling::OneoffService.call(invoice_number: invoice.number.to_s, customer_url: ONE_OFF_CUSTOMER_URL, amount: invoice.total) - create_reserve_domain_invoice(invoice.number, available_names) - build_response(result, invoice.number) + if oneoff_result.code == HTTP_OK || oneoff_result.code == HTTP_CREATED + create_reserve_domain_invoice(invoice.number, available_names, success_business_registry_customer_url, failed_business_registry_customer_url) + end + + build_response(oneoff_result, invoice.number) end def is_any_available_domains?(domain_names) @@ -80,10 +85,12 @@ def process_invoice(invoice) EisBilling::AddDeposits.new(invoice).call end - def create_reserve_domain_invoice(invoice_number, domain_names) + def create_reserve_domain_invoice(invoice_number, domain_names, success_business_registry_customer_url, failed_business_registry_customer_url) create( invoice_number: invoice_number, - domain_names: domain_names + domain_names: domain_names, + success_business_registry_customer_url: success_business_registry_customer_url, + failed_business_registry_customer_url: failed_business_registry_customer_url ) end @@ -92,7 +99,7 @@ def build_response(result, invoice_number) InvoiceResponseStruct.new( status_code_success: success_status?(result.code), - linkpay: parsed_result['everypay_link'], + oneoff_payment_link: parsed_result['oneoff_redirect_link'], invoice_number: invoice_number, details: parsed_result ) diff --git a/app/services/eis_billing/oneoff_service.rb b/app/services/eis_billing/oneoff_service.rb new file mode 100644 index 0000000000..5d177c2aa5 --- /dev/null +++ b/app/services/eis_billing/oneoff_service.rb @@ -0,0 +1,39 @@ +module EisBilling + class OneoffService < EisBilling::Base + + attr_reader :invoice_number, :customer_url, :amount + + def initialize(invoice_number:, customer_url:, amount: nil) + @invoice_number = invoice_number + @customer_url = customer_url + @amount = amount + end + + def self.call(invoice_number:, customer_url:, amount: nil) + new(invoice_number: invoice_number, customer_url: customer_url, amount: amount).call + end + + def call + send_request + end + + private + + def send_request + http = EisBilling::Base.base_request + http.post(invoice_oneoff_url, params.to_json, EisBilling::Base.headers) + end + + def params + { + invoice_number: invoice_number, + customer_url: customer_url, + amount: amount + } + end + + def invoice_oneoff_url + '/api/v1/invoice_generator/oneoff' + end + end +end diff --git a/db/migrate/20241104104620_add_callback_urls_to_reserve_domain_invoices.rb b/db/migrate/20241104104620_add_callback_urls_to_reserve_domain_invoices.rb new file mode 100644 index 0000000000..6042513450 --- /dev/null +++ b/db/migrate/20241104104620_add_callback_urls_to_reserve_domain_invoices.rb @@ -0,0 +1,6 @@ +class AddCallbackUrlsToReserveDomainInvoices < ActiveRecord::Migration[6.1] + def change + add_column :reserve_domain_invoices, :success_business_registry_customer_url, :string + add_column :reserve_domain_invoices, :failed_business_registry_customer_url, :string + end +end diff --git a/db/structure.sql b/db/structure.sql index 04f5c13f7d..b6d20b9d9d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2635,7 +2635,9 @@ CREATE TABLE public.reserve_domain_invoices ( invoice_number character varying, domain_names character varying[] DEFAULT '{}'::character varying[], created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL + updated_at timestamp(6) without time zone NOT NULL, + success_business_registry_customer_url character varying, + failed_business_registry_customer_url character varying ); @@ -5650,6 +5652,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230711083811'), ('20240816091049'), ('20240816092636'), -('20241030095636'); +('20241030095636'), +('20241104104620'); diff --git a/test/integration/api/business_registry/long_reserve_domains_controller_test.rb b/test/integration/api/business_registry/long_reserve_domains_controller_test.rb index 3c49abfca4..c3d43b9987 100644 --- a/test/integration/api/business_registry/long_reserve_domains_controller_test.rb +++ b/test/integration/api/business_registry/long_reserve_domains_controller_test.rb @@ -3,24 +3,33 @@ class LongReserveDomainsControllerTest < ApplicationIntegrationTest def setup @valid_domain_names = ['example1.test', 'example2.test'] + @success_url = 'https://success.test' + @failed_url = 'https://failed.test' @allowed_origins = ['http://example.com', 'https://test.com'] ENV['ALLOWED_ORIGINS'] = @allowed_origins.join(',') + stub_invoice_number_request + stub_add_deposits_request + stub_oneoff_request + @valid_ip = '127.0.0.1' - @invalid_ip = '192.168.1.1' ENV['auction_api_allowed_ips'] = @valid_ip end test "should create long reserve domains with valid parameters" do mock_result = OpenStruct.new( status_code_success: true, - linkpay: "http://payment.test", + oneoff_payment_link: "http://payment.test", invoice_number: "123456" ) ReserveDomainInvoice.stub :create_list_of_domains, mock_result do post api_v1_business_registry_long_reserve_domains_path, - params: { domain_names: @valid_domain_names }, + params: { + domain_names: @valid_domain_names, + success_business_registry_customer_url: @success_url, + failed_business_registry_customer_url: @failed_url + }, headers: { 'Origin' => @allowed_origins.first, 'REMOTE_ADDR' => @valid_ip @@ -30,9 +39,8 @@ def setup json_response = JSON.parse(response.body) assert_equal "Domains are in pending status. Need to pay for domains.", json_response['message'] - assert_equal "http://payment.test", json_response['linkpay'] + assert_equal "http://payment.test", json_response['oneoff_payment_link'] assert_equal "123456", json_response['invoice_number'] - assert_equal @allowed_origins.first, response.headers['Access-Control-Allow-Origin'] end end @@ -110,4 +118,91 @@ def setup assert_equal "Failed to reserve domains", json_response['error'] end end + + test "should handle missing callback urls" do + post api_v1_business_registry_long_reserve_domains_path, + params: { domain_names: @valid_domain_names }, + headers: { + 'Origin' => @allowed_origins.first, + 'REMOTE_ADDR' => @valid_ip + } + + assert_response :created + json_response = JSON.parse(response.body) + assert_not_nil json_response['oneoff_payment_link'] + end + + test "should return error when success URL is invalid" do + post api_v1_business_registry_long_reserve_domains_path, + params: { + domain_names: @valid_domain_names, + success_business_registry_customer_url: "invalid-url" + }, + headers: { + 'Origin' => @allowed_origins.first, + 'REMOTE_ADDR' => @valid_ip + } + + assert_response :bad_request + json_response = JSON.parse(response.body) + assert_equal "Invalid success URL format", json_response['error'] + end + + test "should return error when failed URL is invalid" do + post api_v1_business_registry_long_reserve_domains_path, + params: { + domain_names: @valid_domain_names, + failed_business_registry_customer_url: "invalid-url" + }, + headers: { + 'Origin' => @allowed_origins.first, + 'REMOTE_ADDR' => @valid_ip + } + + assert_response :bad_request + json_response = JSON.parse(response.body) + assert_equal "Invalid failed URL format", json_response['error'] + end + + test "should accept request with valid URLs" do + mock_result = OpenStruct.new( + status_code_success: true, + oneoff_payment_link: "http://payment.test", + invoice_number: "123456" + ) + + ReserveDomainInvoice.stub :create_list_of_domains, mock_result do + post api_v1_business_registry_long_reserve_domains_path, + params: { + domain_names: @valid_domain_names, + success_business_registry_customer_url: "https://success.example.com", + failed_business_registry_customer_url: "https://failed.example.com" + }, + headers: { + 'Origin' => @allowed_origins.first, + 'REMOTE_ADDR' => @valid_ip + } + + assert_response :created + json_response = JSON.parse(response.body) + assert_not_nil json_response['oneoff_payment_link'] + end + end + + private + + def stub_invoice_number_request + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_number_generator") + .to_return(status: 200, body: { invoice_number: '12345' }.to_json, headers: {}) + end + + def stub_add_deposits_request + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_generator") + .to_return(status: 201, body: { everypay_link: 'https://pay.test' }.to_json) + end + + def stub_oneoff_request + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .to_return(status: 200, body: { oneoff_redirect_link: 'https://payment.test' }.to_json) + end end diff --git a/test/integration/api/business_registry/reserve_controller_test.rb b/test/integration/api/business_registry/reserve_controller_test.rb deleted file mode 100644 index 6060e8bec0..0000000000 --- a/test/integration/api/business_registry/reserve_controller_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'test_helper' -require 'webmock/minitest' - -class Api::V1::BusinessRegistry::ReserveControllerTest < ActionDispatch::IntegrationTest - def setup - @valid_domain = 'newdomain.test' - @existing_domain = reserved_domains(:one).name - @valid_params = { - domain_name: @valid_domain - } - @allowed_origins = ['http://example.com', 'https://test.com'] - ENV['ALLOWED_ORIGINS'] = @allowed_origins.join(',') - @valid_ip = '127.0.0.1' - ENV['auction_api_allowed_ips'] = @valid_ip - - stub_invoice_number_request - stub_add_deposits_request - end - - test "should reserve a new domain" do - assert_difference('ReservedDomainStatus.count') do - post api_v1_business_registry_reserve_path, - params: @valid_params, - headers: { 'Origin' => @allowed_origins.first, 'REMOTE_ADDR' => @valid_ip } - end - - assert_response :created - json_response = JSON.parse(response.body) - assert_not_nil json_response['token'] - assert_not_nil json_response['linkpay'] - end - - test "should return existing domain if already reserved" do - assert_no_difference('ReservedDomainStatus.count') do - post api_v1_business_registry_reserve_path, - params: @valid_params.merge(domain_name: @existing_domain), - headers: { 'Origin' => @allowed_origins.first, 'REMOTE_ADDR' => @valid_ip } - end - - assert_response :ok - json_response = JSON.parse(response.body) - assert_equal "Domain already reserved", json_response['message'] - end - - test "should handle errors when saving ReservedDomainStatus" do - ReservedDomainStatus.stub_any_instance(:save, false) do - post api_v1_business_registry_reserve_path, - params: @valid_params, - headers: { 'Origin' => @allowed_origins.first, 'REMOTE_ADDR' => @valid_ip } - - assert_response :unprocessable_entity - json_response = JSON.parse(response.body) - assert_equal "Failed to reserve domain", json_response['error'] - assert_not_nil json_response['details'] - end - end - - test "should handle missing parameters" do - post api_v1_business_registry_reserve_path, - params: {}, - headers: { 'Origin' => @allowed_origins.first, 'REMOTE_ADDR' => @valid_ip } - - assert_response :bad_request - json_response = JSON.parse(response.body) - assert_equal "Missing required parameter: name", json_response['error'] - end - - test "should handle unauthorized origin" do - post api_v1_business_registry_reserve_path, - params: @valid_params, - headers: { 'Origin' => 'http://unauthorized.com', 'REMOTE_ADDR' => @valid_ip } - - assert_response :unauthorized - json_response = JSON.parse(response.body) - assert_equal "Unauthorized origin", json_response['error'] - end - - private - - def stub_invoice_number_request - stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_number_generator") - .to_return(status: 200, body: { invoice_number: '12345' }.to_json, headers: { 'Content-Type' => 'application/json' }) - end - - def stub_add_deposits_request - stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_generator") - .to_return(status: 201, body: { everypay_link: 'https://pay.example.com' }.to_json, headers: { 'Content-Type' => 'application/json' }) - end -end diff --git a/test/models/reserve_domain_invoice_test.rb b/test/models/reserve_domain_invoice_test.rb index 01b71b4ad2..0db7002c06 100644 --- a/test/models/reserve_domain_invoice_test.rb +++ b/test/models/reserve_domain_invoice_test.rb @@ -3,29 +3,58 @@ class ReserveDomainInvoiceTest < ActiveSupport::TestCase def setup @domain_names = ['example1.test', 'example2.test'] + @success_url = 'https://success.test' + @failed_url = 'https://failed.test' + stub_invoice_number_request stub_add_deposits_request + stub_oneoff_request end test "creates list of domains successfully" do - result = ReserveDomainInvoice.create_list_of_domains(@domain_names) + result = ReserveDomainInvoice.create_list_of_domains( + @domain_names, + @success_url, + @failed_url + ) assert result.status_code_success - assert_not_nil result.linkpay + assert_not_nil result.oneoff_payment_link assert_not_nil result.invoice_number + + invoice = ReserveDomainInvoice.last + assert_equal @success_url, invoice.success_business_registry_customer_url + assert_equal @failed_url, invoice.failed_business_registry_customer_url end test "normalizes domain names" do mixed_case_domains = ['EXAMPLE1.TEST', ' example2.test '] - result = ReserveDomainInvoice.create_list_of_domains(mixed_case_domains) + result = ReserveDomainInvoice.create_list_of_domains( + mixed_case_domains, + @success_url, + @failed_url + ) invoice = ReserveDomainInvoice.last assert_equal ['example1.test', 'example2.test'], invoice.domain_names end + test "handles oneoff service failure" do + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .to_return(status: 422, body: { error: 'Payment failed' }.to_json) + + result = ReserveDomainInvoice.create_list_of_domains( + @domain_names, + @success_url, + @failed_url + ) + + assert_equal 'Payment failed', result.details['error'] + end + test "filters out unavailable domains" do ReservedDomain.create!(name: @domain_names.first) - result = ReserveDomainInvoice.create_list_of_domains(@domain_names) + result = ReserveDomainInvoice.create_list_of_domains(@domain_names, @success_url, @failed_url) invoice = ReserveDomainInvoice.last assert_equal [@domain_names.last], invoice.domain_names @@ -59,4 +88,13 @@ def stub_add_deposits_request stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/invoice_generator") .to_return(status: 201, body: { everypay_link: 'https://pay.test' }.to_json) end + + def stub_oneoff_request + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .to_return( + status: 200, + body: { oneoff_redirect_link: 'https://payment.test' }.to_json, + headers: { 'Content-Type': 'application/json' } + ) + end end diff --git a/test/services/eis_billing/oneoff_service_test.rb b/test/services/eis_billing/oneoff_service_test.rb new file mode 100644 index 0000000000..e6146baa08 --- /dev/null +++ b/test/services/eis_billing/oneoff_service_test.rb @@ -0,0 +1,97 @@ +require 'test_helper' +require 'webmock/minitest' + +class EisBilling::OneoffServiceTest < ActiveSupport::TestCase + def setup + @invoice_number = '12345' + @customer_url = 'https://example.com/success' + @amount = 100.50 + @service = EisBilling::OneoffService.new( + invoice_number: @invoice_number, + customer_url: @customer_url, + amount: @amount + ) + end + + test "initialization sets attributes correctly" do + assert_equal @invoice_number, @service.invoice_number + assert_equal @customer_url, @service.customer_url + assert_equal @amount, @service.amount + end + + test "call method sends POST request with correct parameters" do + expected_response = { everypay_link: 'https://pay.example.com/12345' } + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .with( + body: { + invoice_number: @invoice_number, + customer_url: @customer_url, + amount: @amount + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => /^Bearer .+$/ + } + ) + .to_return(status: 200, body: expected_response.to_json, headers: { 'Content-Type' => 'application/json' }) + + response = @service.call + + assert_equal 200, response.code.to_i + assert_equal expected_response.to_json, response.body + end + + test "class method call creates instance and calls instance method" do + expected_response = { everypay_link: 'https://pay.example.com/12345' } + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .to_return(status: 200, body: expected_response.to_json, headers: { 'Content-Type' => 'application/json' }) + + response = EisBilling::OneoffService.call( + invoice_number: @invoice_number, + customer_url: @customer_url, + amount: @amount + ) + + assert_equal 200, response.code.to_i + assert_equal expected_response.to_json, response.body + end + + test "handles error response" do + error_response = { error: 'Invalid parameters' } + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .to_return(status: 422, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' }) + + response = @service.call + + assert_equal 422, response.code.to_i + assert_equal error_response.to_json, response.body + end + + test "sends request with nil amount" do + service = EisBilling::OneoffService.new( + invoice_number: @invoice_number, + customer_url: @customer_url, + amount: nil + ) + + stub_request(:post, "https://eis_billing_system:3000/api/v1/invoice_generator/oneoff") + .with( + body: { + invoice_number: @invoice_number, + customer_url: @customer_url, + amount: nil + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => /^Bearer .+$/ + } + ) + .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' }) + + response = service.call + assert_equal 200, response.code.to_i + end +end