Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
16.10.2026
* Business Registry reservation invoices get a reference number and due date; invoice PDF is available before payment so it can be paid by bank transfer https://github.com/internetee/registry/pull/2972
* New billing endpoint PUT /eis_billing/business_registry_payment reserves domains when EIS billing matches a bank transfer by reference number https://github.com/internetee/registry/pull/2972
* Reservation is first-come-first-served: unpaid invoice PDF warns that it does not guarantee the reservation; a bank payment for domains that are no longer available cancels the invoice and EIS billing asks admin to refund it https://github.com/internetee/registry/pull/2972
* Fixed cancelling of intersecting reservation invoices after a paid callback https://github.com/internetee/registry/pull/2972
* Deploy: run migrations; set business_registry_payment_url in eis_billing_system (https://github.com/internetee/eis_billing_system/pull/268)

15.10.2026
* Fix for whois duplicate records https://github.com/internetee/registry/pull/2966

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def create
linkpay_url: decode_linkpay_url(result.linkpay_url),
invoice_number: result.invoice_number,
user_unique_id: result.user_unique_id,
reference_no: result.reference_no,
due_date: result.due_date,
available_domains: ReserveDomainInvoice.filter_available_domains(@domain_names)
}, :created)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ class ReservedDomainsInvoicePdfController < BaseController
include ReservedDomainInvoiceScoped

def show
result = @reserved_domain_invoice.invoice_state
paid = @reserved_domain_invoice.invoice_state.paid?

unless result.paid?
render_error('Invoice is not paid', :unprocessable_entity, { status: result.status, message: result.message })
unless paid || @reserved_domain_invoice.pending?
render_error('Invoice is cancelled', :unprocessable_entity, { status: @reserved_domain_invoice.status })
return
end

send_data @reserved_domain_invoice.as_pdf(invoice_context),
send_data @reserved_domain_invoice.as_pdf(invoice_context.merge(paid: paid)),
filename: "invoice-#{@reserved_domain_invoice.invoice_number}.pdf",
type: 'application/pdf',
disposition: 'attachment'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,15 @@ def callback

if result.code == '200'
reserve_domain_invoice = ReserveDomainInvoice.find_by(invoice_number: invoice_number, metainfo: user_unique_id)
filtered_domains = BusinessRegistry::DomainAvailabilityCheckerService.filter_available(reserve_domain_invoice.domain_names)

unless filtered_domains.count == reserve_domain_invoice.domain_names.count
return render status: :unprocessable_entity, json: { message: 'Failed to reserve domains' }
if reserve_domain_invoice.reserve_paid_domains == :reserved
render status: :ok, json: { message: 'Callback received' }
else
render status: :unprocessable_entity, json: { message: 'Failed to reserve domains' }
end

reserved_domains = []
ReservedDomain::Audit.set(reason: 'paid_reservation', reason_note: nil, registrar_id: nil) do
filtered_domains.each do |domain|
reserved_domains << ReservedDomain.create(
name: domain,
expire_at: ReservedDomain.expire_at_for(ReservedDomain::PAID_RESERVATION_EXPIRY)
)
end
end
reserve_domain_invoice.paid!
ReserveDomainInvoice.cancel_intersecting_invoices(reserve_domain_invoice)

render status: :ok, json: { message: 'Callback received' }
else
render status: :internal_server_error, json: { message: 'Callback failed' }
end

end

private
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module EisBilling
class BusinessRegistryPaymentsController < EisBilling::BaseController
around_action :set_audit_context

def update
reserve_domain_invoice = find_reserve_domain_invoice
return render status: :not_found, json: { message: 'Invoice not found' } if reserve_domain_invoice.nil?

if reserve_domain_invoice.cancelled? || reserve_domain_invoice.failed?
return render status: :unprocessable_entity, json: { message: 'Invoice is cancelled' }
end

case reserve_domain_invoice.reserve_paid_domains
when :reserved
render status: :ok, json: { message: 'Payment processed' }
when :already_paid
render status: :ok, json: { message: 'Invoice already paid' }
else
# First-come-first-served: billing refunds the payment of a cancelled invoice
reserve_domain_invoice.cancelled!
render status: :unprocessable_entity, json: { message: 'Failed to reserve domains' }
end
end

private

def find_reserve_domain_invoice
return if params[:reference_number].blank?

ReserveDomainInvoice.find_by(reference_no: params[:reference_number])
end

def set_audit_context
::PaperTrail.request(whodunnit: 'EIS billing payment') do
ReservedDomain::Audit.set(source: 'eis_billing', reason: nil,
reason_note: nil, registrar_id: nil) { yield }
end
end
end
end
63 changes: 48 additions & 15 deletions app/models/reserve_domain_invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class InvoiceStruct < Struct.new(:total, :number, :buyer_name, :buyer_email,
end

class InvoiceResponseStruct < Struct.new(:status_code_success, :linkpay_url, :invoice_number, :details, :reserved_domain_names, :user_unique_id,
keyword_init: true)
:reference_no, :due_date, keyword_init: true)
end

INITIATOR = 'business_registry'
Expand All @@ -30,18 +30,20 @@ def create_list_of_domains(domain_names)

return build_error_response('Some intersecting invoices are paid') if is_any_intersecting_invoice_paid?(available_names)

user_unique_id = generate_unique_id
user_unique_id = generate_unique_id
reference_no = fetch_reference_no(user_unique_id)

invoice = if are_domains_intersect?(available_names)
invoice_number = get_invoice_number_from_intersecting_invoice(available_names).to_i
create_invoice_with_domains(available_names, invoice_number: invoice_number, user_unique_id: user_unique_id)
create_invoice_with_domains(available_names, invoice_number: invoice_number, user_unique_id: user_unique_id,
reference_no: reference_no)
else
create_invoice_with_domains(available_names, user_unique_id: user_unique_id)
create_invoice_with_domains(available_names, user_unique_id: user_unique_id, reference_no: reference_no)
end

result = process_invoice(invoice)
create_reserve_domain_invoice(invoice.number, available_names, invoice.user_unique_id)
build_response(result, invoice.number, invoice.user_unique_id, available_names)
reserve_domain_invoice = create_reserve_domain_invoice(invoice, available_names)
build_response(result, invoice, reserve_domain_invoice)
end

def are_domains_intersect?(domain_names)
Expand Down Expand Up @@ -100,12 +102,13 @@ def normalize_name(name)
SimpleIDN.to_unicode(name).mb_chars.downcase.strip.to_s
end

def create_invoice_with_domains(domain_names, invoice_number: nil, user_unique_id: nil)
def create_invoice_with_domains(domain_names, invoice_number: nil, user_unique_id: nil, reference_no: nil)
invoice_number = fetch_invoice_number if invoice_number.nil?

build_invoice(
total: domain_price_calculation(domain_names),
number: invoice_number,
reference_no: reference_no,
reserved_domain_names: domain_names,
user_unique_id: user_unique_id
)
Expand All @@ -116,6 +119,11 @@ def fetch_invoice_number
JSON.parse(response.body).fetch('invoice_number').to_i
end

def fetch_reference_no(owner)
response = EisBilling::GetReferenceNumber.call(owner: owner, initiator: INITIATOR)
JSON.parse(response.body).fetch('reference_number')
end

def build_invoice(attributes)
InvoiceStruct.new(
total: attributes[:total],
Expand All @@ -124,7 +132,7 @@ def build_invoice(attributes)
buyer_email: nil,
description: 'description',
initiator: INITIATOR,
reference_no: nil,
reference_no: attributes[:reference_no],
reserved_domain_names: attributes[:reserved_domain_names],
user_unique_id: attributes[:user_unique_id]
)
Expand All @@ -138,24 +146,28 @@ def process_invoice(invoice)
EisBilling::AddDeposits.new(invoice).call
end

def create_reserve_domain_invoice(invoice_number, domain_names, user_unique_id)
def create_reserve_domain_invoice(invoice, domain_names)
create(
invoice_number: invoice_number,
invoice_number: invoice.number,
domain_names: domain_names,
metainfo: user_unique_id
metainfo: invoice.user_unique_id,
reference_no: invoice.reference_no,
due_date: (Time.zone.now + Setting.days_to_keep_invoices_active.days).to_date
)
end

def build_response(result, invoice_number, user_unique_id, domain_names)
def build_response(result, invoice, reserve_domain_invoice)
parsed_result = JSON.parse(result.body)
# link = JSON.parse(result.body)['everypay_link']

InvoiceResponseStruct.new(
status_code_success: success_status?(result.code),
linkpay_url: parsed_result['everypay_link'],
invoice_number: invoice_number,
user_unique_id: user_unique_id,
reserved_domain_names: domain_names,
invoice_number: invoice.number,
user_unique_id: invoice.user_unique_id,
reserved_domain_names: invoice.reserved_domain_names,
reference_no: reserve_domain_invoice.reference_no,
due_date: reserve_domain_invoice.due_date,
details: parsed_result
)
end
Expand Down Expand Up @@ -197,6 +209,22 @@ def create_paid_reserved_domains
end
end

# Returns :reserved, :already_paid or :unavailable (nothing is reserved when any domain is taken)
def reserve_paid_domains
with_lock do
if paid?
:already_paid
elsif all_domains_available?
create_paid_reserved_domains
paid!
self.class.cancel_intersecting_invoices(domain_names)
:reserved
else
:unavailable
end
end
end

def build_reserved_domains_output
domain_names.map do |name|
domain = ReservedDomain.find_by(name: name)
Expand All @@ -212,4 +240,9 @@ def build_reserved_domains_output
end
end

private

def all_domains_available?
self.class.filter_available_domains(domain_names).size == domain_names.size
end
end
15 changes: 13 additions & 2 deletions app/models/reserve_domain_invoice/pdf_generator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class ReserveDomainInvoice
class PdfGenerator
attr_reader :invoice, :customer_name, :customer_address, :customer_vat_no, :payment_date
attr_reader :invoice, :customer_name, :customer_address, :customer_vat_no

delegate :reference_no, :due_date, to: :invoice

def initialize(invoice, context = {})
@invoice = invoice
Expand All @@ -9,6 +11,15 @@ def initialize(invoice, context = {})
@customer_vat_no = context[:customer_vat_no]
@private_individual = context[:private_individual]
@payment_date = context[:payment_date]
@paid = context[:paid]
end

def paid?
!!@paid
end

def payment_date
@payment_date if paid?
end

def amount_paid
Expand All @@ -20,7 +31,7 @@ def as_pdf
end

def rows
invoice.domain_names.map { |name| { name: name, reserved: true, price: net_price_per_domain } }
invoice.domain_names.map { |name| { name: name, reserved: paid?, price: net_price_per_domain } }
end

def rows_sum
Expand Down
11 changes: 6 additions & 5 deletions app/services/eis_billing/get_reference_number.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
module EisBilling
class GetReferenceNumber < EisBilling::Base
attr_reader :owner
attr_reader :owner, :initiator

# rubocop:disable Lint/MissingSuper
def initialize(owner:)
def initialize(owner:, initiator: INITIATOR)
@owner = owner
@initiator = initiator
end

def self.call(owner:)
new(owner: owner).call
def self.call(owner:, initiator: INITIATOR)
new(owner: owner, initiator: initiator).call
end

def call
Expand All @@ -18,7 +19,7 @@ def call

def payload
{
initiator: INITIATOR,
initiator: initiator,
owner: owner
}
end
Expand Down
38 changes: 34 additions & 4 deletions app/views/reserve_domain_invoices/pdf.haml
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,23 @@
.row
.col-md-6.left
%dl.dl-horizontal
%dt Arve number
%dd= @pdf.invoice.invoice_number

%dt Arve kuupäev
%dd= l(@pdf.invoice_date)

- if @pdf.due_date
%dt Maksetähtaeg
%dd= l(@pdf.due_date)

- if @pdf.reference_no.present?
%dt Viitenumber
%dd= @pdf.reference_no

%dt Makse staatus
%dd= @pdf.paid? ? 'Tasutud' : 'Tasumata'

- if @pdf.payment_date
%dt Makse kuupäev
%dd= l(@pdf.payment_date)
Expand Down Expand Up @@ -190,7 +204,7 @@
- @pdf.rows.each do |row|
%tr
%td= row[:name]
%td= row[:reserved] ? 'Reserveeritud' : 'Reserveerimine ebaõnnestus'
%td= row[:reserved] ? 'Reserveeritud' : 'Ootab tasumist'
%td.text-right= eur.(row[:price])
%tfoot
%tr
Expand All @@ -202,9 +216,25 @@
%tr
%th.no-border.text-right{colspan: 2} Summa kokku
%td.text-right= eur.(@pdf.grand_total)
%tr
%th.no-border.text-right{colspan: 2} Tasutud summa
%td.text-right= eur.(@pdf.amount_paid)
- if @pdf.paid?
%tr
%th.no-border.text-right{colspan: 2} Tasutud summa
%td.text-right= eur.(@pdf.amount_paid)
- else
%tr
%th.no-border.text-right{colspan: 2} Tasuda
%td.text-right= eur.(@pdf.grand_total)

- unless @pdf.paid?
%p
Arve väljastamine ei taga domeeninimede broneerimist. Broneerimine, nagu ka
registreerimine, toimub põhimõttel "kes ees, see mees". Kui makse laekumise ajaks
ei ole broneerimine enam võimalik, arve tühistatakse ja makstud summa tagastatakse.
%p
Issuing this invoice does not guarantee the reservation of the domain names.
Reservation, like registration, is first-come-first-served. If the reservation is
no longer possible when the payment arrives, the invoice is cancelled and the
payment is refunded.

#footer
%hr
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
put '/e_invoice_response', to: 'e_invoice_response#update', as: 'e_invoice_response'
post '/lhv_connect_transactions', to: 'lhv_connect_transactions#create', as: 'lhv_connect_transactions'
get 'callback', to: 'business_registry_callback#callback', as: 'callback'
put '/business_registry_payment', to: 'business_registry_payments#update', as: 'business_registry_payment'
resource :invoices, only: [:update]
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AddReferenceNoAndDueDateToReserveDomainInvoices < ActiveRecord::Migration[6.1]
disable_ddl_transaction!

def change
add_column :reserve_domain_invoices, :reference_no, :string
add_column :reserve_domain_invoices, :due_date, :date
add_index :reserve_domain_invoices, :reference_no, unique: true, where: 'reference_no IS NOT NULL',
algorithm: :concurrently
end
end
Loading
Loading