Created job for sending monthly invoices

This commit is contained in:
Sergei Tsõganov 2022-08-21 19:11:39 +03:00
parent a5f803b57a
commit d589aa1681
18 changed files with 1103 additions and 43 deletions

View file

@ -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

View file

@ -17,7 +17,7 @@ class SendEInvoiceJob < ApplicationJob
def need_to_process_invoice?(invoice:, payable:) def need_to_process_invoice?(invoice:, payable:)
logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}" logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}"
return false if invoice.blank? return false if invoice.blank?
return false if invoice.do_not_send_e_invoice? && payable return false if invoice.do_not_send_e_invoice? && (invoice.monthly_invoice ? true : payable)
true true
end end

View file

@ -0,0 +1,155 @@
class SendMonthlyInvoicesJob < ApplicationJob
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
def send_monthly_invoices
Registrar.where.not(test_registrar: true).find_each do |registrar|
next unless registrar.cash_account
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
InvoiceMailer.invoice_email(invoice: invoice,
recipient: registrar.billing_email)
.deliver_now
SendEInvoiceJob.set(wait: 1.minute).perform_now(invoice.id, payable: false)
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
def create_invoice(summary, registrar)
vat_rate = ::Invoice::VatRateCalculator.new(registrar: registrar).calculate
invoice = Invoice.new(
number: assign_monthly_number,
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: registrar,
buyer_name: registrar.name,
buyer_reg_no: registrar.reg_no,
buyer_country_code: registrar.address_country_code,
buyer_state: registrar.address_state,
buyer_street: registrar.address_street,
buyer_city: registrar.address_city,
buyer_zip: registrar.address_zip,
buyer_phone: registrar.phone,
buyer_url: registrar.website,
buyer_email: registrar.email,
reference_no: registrar.reference_no,
vat_rate: vat_rate,
monthly_invoice: true,
metadata: { items: summary['invoice_lines'] },
total: 0
)
return unless invoice.save!
update_directo_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_numbers
invoices_count = @directo_client.invoices.count
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?(invoices_count,
last_directo_num)
@directo_client.invoices.each do |inv|
last_directo_num += 1
inv.number = last_directo_num
end
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(res: res, req: req, invoice: inv)
end
end
def mark_invoice_as_sent(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_directo_number(num:)
return unless num.to_i > Setting.directo_monthly_number_last.to_i
Setting.directo_monthly_number_last = num.to_i
end
end

View file

@ -4,6 +4,7 @@ class InvoiceMailer < ApplicationMailer
subject = default_i18n_subject(invoice_number: invoice.number) subject = default_i18n_subject(invoice_number: invoice.number)
subject << I18n.t('invoice.already_paid') if paid 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 attachments["invoice-#{invoice.number}.pdf"] = invoice.as_pdf
mail(to: recipient, subject: subject) mail(to: recipient, subject: subject)
end end

View file

@ -55,6 +55,11 @@ module Registrar::BookKeeping
.where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW])
end 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) def new_monthly_invoice_line(activity:, duration: nil)
price = load_price(activity) price = load_price(activity)
line = { line = {
@ -68,7 +73,7 @@ module Registrar::BookKeeping
def finalize_invoice_line(line, price:, activity:, duration:) def finalize_invoice_line(line, price:, activity:, duration:)
yearly = price.duration.in_years.to_i >= 1 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) line['description'] = description_in_language(price: price, yearly: yearly)
add_product_timeframe(line: line, activity: activity, duration: duration) if duration.present? && (duration > 1) add_product_timeframe(line: line, activity: activity, duration: duration) if duration.present? && (duration > 1)

View file

@ -32,11 +32,14 @@ class Invoice < ApplicationRecord
# rubocop:enable Layout/LineLength # rubocop:enable Layout/LineLength
# rubocop:enable Style/MultilineBlockLayout # rubocop:enable Style/MultilineBlockLayout
validates :due_date, :currency, :seller_name, 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 :set_invoice_number
before_create :calculate_total, unless: :total? before_create :calculate_total, unless: :total?
before_create :apply_default_buyer_vat_no, unless: :buyer_vat_no? 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 attribute :vat_rate, ::Type::VatRate.new
@ -118,7 +121,7 @@ class Invoice < ApplicationRecord
end end
def subtotal def subtotal
items.map(&:item_sum_without_vat).reduce(:+) items.map(&:item_sum_without_vat).reduce(:+) || 0
end end
def vat_amount def vat_amount
@ -131,7 +134,11 @@ class Invoice < ApplicationRecord
end end
def each(&block) def each(&block)
items.each(&block) if monthly_invoice
metadata['items'].map { |el| OpenStruct.new(el) }.each(&block)
else
items.each(&block)
end
end end
def as_pdf def as_pdf

View file

@ -46,10 +46,17 @@ class Invoice
i.price = invoice_item.price i.price = invoice_item.price
i.quantity = invoice_item.quantity i.quantity = invoice_item.quantity
i.unit = invoice_item.unit i.unit = invoice_item.unit
i.subtotal = invoice_item.subtotal if invoice.monthly_invoice
i.vat_rate = invoice_item.vat_rate i.subtotal = 0
i.vat_amount = invoice_item.vat_amount i.vat_rate = 0
i.total = invoice_item.total i.vat_amount = 0
i.total = 0
else
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
end end
e_invoice_invoice_items << e_invoice_invoice_item e_invoice_invoice_items << e_invoice_invoice_item
end end
@ -66,9 +73,15 @@ class Invoice
i.beneficiary_name = invoice.seller_name i.beneficiary_name = invoice.seller_name
i.beneficiary_account_number = invoice.seller_iban i.beneficiary_account_number = invoice.seller_iban
i.payer_name = invoice.buyer_name i.payer_name = invoice.buyer_name
i.subtotal = invoice.subtotal if invoice.monthly_invoice
i.vat_amount = invoice.vat_amount i.subtotal = 0
i.total = invoice.total i.vat_amount = 0
i.total = 0
else
i.subtotal = invoice.subtotal
i.vat_amount = invoice.vat_amount
i.total = invoice.total
end
i.currency = invoice.currency i.currency = invoice.currency
i.delivery_channel = %i[internet_bank portal] i.delivery_channel = %i[internet_bank portal]
i.payable = payable i.payable = payable

View file

@ -14,7 +14,8 @@ class Invoice
private private
def invoice_html 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 end
end end

View file

@ -4,11 +4,12 @@
= @invoice = @invoice
.col-sm-8 .col-sm-8
%h1.text-right.text-center-xs %h1.text-right.text-center-xs
- if @invoice.unpaid? - unless @invoice.monthly_invoice
= link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default') - 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? - 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(: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('.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') = link_to(t('.deliver_btn'), new_admin_invoice_delivery_path(@invoice), class: 'btn btn-default')

View file

@ -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

View file

@ -234,7 +234,7 @@
%td= invoice_item.unit %td= invoice_item.unit
%td= invoice_item.quantity %td= invoice_item.quantity
%td= currency(invoice_item.price) %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 %tfoot
%tr %tr
%th{colspan: 3} %th{colspan: 3}

View file

@ -484,6 +484,8 @@ en:
invoice: invoice:
title: 'Invoice' title: 'Invoice'
already_paid: " (already paid)" already_paid: " (already paid)"
monthly_invoice: " (monthly invoice)"
monthly_invoice_description: 'Monthly invoice'
bank_statements: 'Bank statements' bank_statements: 'Bank statements'
back_to_bank_statements: 'Back to bank statements' back_to_bank_statements: 'Back to bank statements'
back_to_bank_statement: 'Back to bank statement' back_to_bank_statement: 'Back to bank statement'

View file

@ -9,3 +9,5 @@ et:
invoice: invoice:
title: 'Arve' title: 'Arve'
already_paid: " (juba makstud)" already_paid: " (juba makstud)"
monthly_invoice: " (kuuaruanne)"
monthly_invoice_description: 'Kuuaruanne'

View file

@ -0,0 +1,6 @@
class AddMonthlyInvoiceTypeColumns < ActiveRecord::Migration[6.1]
def change
add_column :invoices, :monthly_invoice, :boolean, default: false
add_column :invoices, :metadata, :jsonb
end
end

View file

@ -955,14 +955,14 @@ CREATE TABLE public.domains (
pending_json jsonb, pending_json jsonb,
force_delete_date date, force_delete_date date,
statuses character varying[], statuses character varying[],
status_notes public.hstore,
upid integer, upid integer,
up_date timestamp without time zone, up_date timestamp without time zone,
uuid uuid DEFAULT public.gen_random_uuid() NOT NULL, uuid uuid DEFAULT public.gen_random_uuid() NOT NULL,
locked_by_registrant_at timestamp without time zone, locked_by_registrant_at timestamp without time zone,
force_delete_start timestamp without time zone, force_delete_start timestamp without time zone,
force_delete_data public.hstore, force_delete_data public.hstore,
json_statuses_history jsonb, json_statuses_history jsonb
status_notes public.hstore
); );
@ -985,6 +985,98 @@ CREATE SEQUENCE public.domains_id_seq
ALTER SEQUENCE public.domains_id_seq OWNED BY public.domains.id; ALTER SEQUENCE public.domains_id_seq OWNED BY public.domains.id;
--
-- Name: email_address_verifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.email_address_verifications (
id bigint NOT NULL,
email public.citext NOT NULL,
verified_at timestamp without time zone,
success boolean DEFAULT false NOT NULL,
domain public.citext NOT NULL
);
--
-- Name: email_address_verifications_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.email_address_verifications_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: email_address_verifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.email_address_verifications_id_seq OWNED BY public.email_address_verifications.id;
--
-- Name: email_addresses_validations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.email_addresses_validations (
id bigint NOT NULL,
email character varying NOT NULL,
validated_at timestamp without time zone
);
--
-- Name: email_addresses_validations_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.email_addresses_validations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: email_addresses_validations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.email_addresses_validations_id_seq OWNED BY public.email_addresses_validations.id;
--
-- Name: email_addresses_verifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.email_addresses_verifications (
id bigint NOT NULL,
email character varying NOT NULL,
validated_at timestamp without time zone
);
--
-- Name: email_addresses_verifications_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.email_addresses_verifications_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: email_addresses_verifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.email_addresses_verifications_id_seq OWNED BY public.email_addresses_verifications.id;
-- --
-- Name: epp_sessions; Type: TABLE; Schema: public; Owner: - -- Name: epp_sessions; Type: TABLE; Schema: public; Owner: -
-- --
@ -1104,6 +1196,8 @@ CREATE TABLE public.invoices (
issue_date date NOT NULL, issue_date date NOT NULL,
e_invoice_sent_at timestamp without time zone, e_invoice_sent_at timestamp without time zone,
payment_link character varying, payment_link character varying,
monthly_invoice boolean DEFAULT false,
metadata jsonb,
CONSTRAINT invoices_due_date_is_not_before_issue_date CHECK ((due_date >= issue_date)) CONSTRAINT invoices_due_date_is_not_before_issue_date CHECK ((due_date >= issue_date))
); );
@ -2190,6 +2284,74 @@ CREATE SEQUENCE public.payment_orders_id_seq
ALTER SEQUENCE public.payment_orders_id_seq OWNED BY public.payment_orders.id; ALTER SEQUENCE public.payment_orders_id_seq OWNED BY public.payment_orders.id;
--
-- Name: pghero_query_stats; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.pghero_query_stats (
id bigint NOT NULL,
database text,
"user" text,
query text,
query_hash bigint,
total_time double precision,
calls bigint,
captured_at timestamp without time zone
);
--
-- Name: pghero_query_stats_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.pghero_query_stats_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: pghero_query_stats_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.pghero_query_stats_id_seq OWNED BY public.pghero_query_stats.id;
--
-- Name: pghero_space_stats; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.pghero_space_stats (
id bigint NOT NULL,
database text,
schema text,
relation text,
size bigint,
captured_at timestamp without time zone
);
--
-- Name: pghero_space_stats_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.pghero_space_stats_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: pghero_space_stats_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.pghero_space_stats_id_seq OWNED BY public.pghero_space_stats.id;
-- --
-- Name: prices; Type: TABLE; Schema: public; Owner: - -- Name: prices; Type: TABLE; Schema: public; Owner: -
-- --
@ -2228,6 +2390,48 @@ CREATE SEQUENCE public.prices_id_seq
ALTER SEQUENCE public.prices_id_seq OWNED BY public.prices.id; ALTER SEQUENCE public.prices_id_seq OWNED BY public.prices.id;
--
-- Name: que_jobs; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.que_jobs (
priority smallint DEFAULT 100 NOT NULL,
run_at timestamp with time zone DEFAULT now() NOT NULL,
job_id bigint NOT NULL,
job_class text NOT NULL,
args json DEFAULT '[]'::json NOT NULL,
error_count integer DEFAULT 0 NOT NULL,
last_error text,
queue text DEFAULT ''::text NOT NULL
);
--
-- Name: TABLE que_jobs; Type: COMMENT; Schema: public; Owner: -
--
COMMENT ON TABLE public.que_jobs IS '3';
--
-- Name: que_jobs_job_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.que_jobs_job_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: que_jobs_job_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.que_jobs_job_id_seq OWNED BY public.que_jobs.job_id;
-- --
-- Name: registrant_verifications; Type: TABLE; Schema: public; Owner: - -- Name: registrant_verifications; Type: TABLE; Schema: public; Owner: -
-- --
@ -2508,8 +2712,7 @@ CREATE TABLE public.validation_events (
validation_eventable_type character varying, validation_eventable_type character varying,
validation_eventable_id bigint, validation_eventable_id bigint,
created_at timestamp(6) without time zone NOT NULL, 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
event_type public.validation_type
); );
@ -2813,6 +3016,27 @@ ALTER TABLE ONLY public.domain_transfers ALTER COLUMN id SET DEFAULT nextval('pu
ALTER TABLE ONLY public.domains ALTER COLUMN id SET DEFAULT nextval('public.domains_id_seq'::regclass); ALTER TABLE ONLY public.domains ALTER COLUMN id SET DEFAULT nextval('public.domains_id_seq'::regclass);
--
-- Name: email_address_verifications id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_address_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_address_verifications_id_seq'::regclass);
--
-- Name: email_addresses_validations id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_addresses_validations ALTER COLUMN id SET DEFAULT nextval('public.email_addresses_validations_id_seq'::regclass);
--
-- Name: email_addresses_verifications id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_addresses_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_addresses_verifications_id_seq'::regclass);
-- --
-- Name: epp_sessions id; Type: DEFAULT; Schema: public; Owner: - -- Name: epp_sessions id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -3030,6 +3254,20 @@ ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.payment_orders ALTER COLUMN id SET DEFAULT nextval('public.payment_orders_id_seq'::regclass); ALTER TABLE ONLY public.payment_orders ALTER COLUMN id SET DEFAULT nextval('public.payment_orders_id_seq'::regclass);
--
-- Name: pghero_query_stats id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.pghero_query_stats ALTER COLUMN id SET DEFAULT nextval('public.pghero_query_stats_id_seq'::regclass);
--
-- Name: pghero_space_stats id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.pghero_space_stats ALTER COLUMN id SET DEFAULT nextval('public.pghero_space_stats_id_seq'::regclass);
-- --
-- Name: prices id; Type: DEFAULT; Schema: public; Owner: - -- Name: prices id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -3037,6 +3275,13 @@ ALTER TABLE ONLY public.payment_orders ALTER COLUMN id SET DEFAULT nextval('publ
ALTER TABLE ONLY public.prices ALTER COLUMN id SET DEFAULT nextval('public.prices_id_seq'::regclass); ALTER TABLE ONLY public.prices ALTER COLUMN id SET DEFAULT nextval('public.prices_id_seq'::regclass);
--
-- Name: que_jobs job_id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.que_jobs ALTER COLUMN job_id SET DEFAULT nextval('public.que_jobs_job_id_seq'::regclass);
-- --
-- Name: registrant_verifications id; Type: DEFAULT; Schema: public; Owner: - -- Name: registrant_verifications id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -3274,6 +3519,30 @@ ALTER TABLE ONLY public.domains
ADD CONSTRAINT domains_pkey PRIMARY KEY (id); ADD CONSTRAINT domains_pkey PRIMARY KEY (id);
--
-- Name: email_address_verifications email_address_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_address_verifications
ADD CONSTRAINT email_address_verifications_pkey PRIMARY KEY (id);
--
-- Name: email_addresses_validations email_addresses_validations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_addresses_validations
ADD CONSTRAINT email_addresses_validations_pkey PRIMARY KEY (id);
--
-- Name: email_addresses_verifications email_addresses_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.email_addresses_verifications
ADD CONSTRAINT email_addresses_verifications_pkey PRIMARY KEY (id);
-- --
-- Name: epp_sessions epp_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: epp_sessions epp_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -3522,6 +3791,22 @@ ALTER TABLE ONLY public.payment_orders
ADD CONSTRAINT payment_orders_pkey PRIMARY KEY (id); ADD CONSTRAINT payment_orders_pkey PRIMARY KEY (id);
--
-- Name: pghero_query_stats pghero_query_stats_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.pghero_query_stats
ADD CONSTRAINT pghero_query_stats_pkey PRIMARY KEY (id);
--
-- Name: pghero_space_stats pghero_space_stats_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.pghero_space_stats
ADD CONSTRAINT pghero_space_stats_pkey PRIMARY KEY (id);
-- --
-- Name: prices prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: prices prices_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -3530,6 +3815,14 @@ ALTER TABLE ONLY public.prices
ADD CONSTRAINT prices_pkey PRIMARY KEY (id); ADD CONSTRAINT prices_pkey PRIMARY KEY (id);
--
-- Name: que_jobs que_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.que_jobs
ADD CONSTRAINT que_jobs_pkey PRIMARY KEY (queue, priority, run_at, job_id);
-- --
-- Name: registrant_verifications registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: registrant_verifications registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -3941,6 +4234,13 @@ CREATE INDEX index_domains_on_registrar_id ON public.domains USING btree (regist
CREATE INDEX index_domains_on_statuses ON public.domains USING gin (statuses); CREATE INDEX index_domains_on_statuses ON public.domains USING gin (statuses);
--
-- Name: index_email_address_verifications_on_domain; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_email_address_verifications_on_domain ON public.email_address_verifications USING btree (domain);
-- --
-- Name: index_epp_sessions_on_updated_at; Type: INDEX; Schema: public; Owner: - -- Name: index_epp_sessions_on_updated_at; Type: INDEX; Schema: public; Owner: -
-- --
@ -4277,6 +4577,20 @@ CREATE INDEX index_notifications_on_registrar_id ON public.notifications USING b
CREATE INDEX index_payment_orders_on_invoice_id ON public.payment_orders USING btree (invoice_id); CREATE INDEX index_payment_orders_on_invoice_id ON public.payment_orders USING btree (invoice_id);
--
-- Name: index_pghero_query_stats_on_database_and_captured_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_pghero_query_stats_on_database_and_captured_at ON public.pghero_query_stats USING btree (database, captured_at);
--
-- Name: index_pghero_space_stats_on_database_and_captured_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_pghero_space_stats_on_database_and_captured_at ON public.pghero_space_stats USING btree (database, captured_at);
-- --
-- Name: index_prices_on_zone_id; Type: INDEX; Schema: public; Owner: - -- Name: index_prices_on_zone_id; Type: INDEX; Schema: public; Owner: -
-- --
@ -4333,13 +4647,6 @@ CREATE INDEX index_users_on_registrar_id ON public.users USING btree (registrar_
CREATE INDEX index_validation_events_on_event_data ON public.validation_events USING gin (event_data); CREATE INDEX index_validation_events_on_event_data ON public.validation_events USING gin (event_data);
--
-- Name: index_validation_events_on_event_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_validation_events_on_event_type ON public.validation_events USING btree (event_type);
-- --
-- Name: index_validation_events_on_validation_eventable; Type: INDEX; Schema: public; Owner: - -- Name: index_validation_events_on_validation_eventable; Type: INDEX; Schema: public; Owner: -
-- --
@ -5085,9 +5392,11 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210708131814'), ('20210708131814'),
('20210729131100'), ('20210729131100'),
('20210729134625'), ('20210729134625'),
('20210827185249'), ('20211028122103'),
('20211029073644'), ('20211028125245'),
('20211029082225'),
('20211124071418'), ('20211124071418'),
('20211124084308'),
('20211125181033'), ('20211125181033'),
('20211125184334'), ('20211125184334'),
('20211126085139'), ('20211126085139'),
@ -5098,12 +5407,12 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220124105717'), ('20220124105717'),
('20220228093211'), ('20220228093211'),
('20220316140727'), ('20220316140727'),
('20220406085500'),
('20220412130856'), ('20220412130856'),
('20220413073315'), ('20220413073315'),
('20220413084536'), ('20220413084536'),
('20220413084748'), ('20220413084748'),
('20220504090512'), ('20220504090512'),
('20220524130709'); ('20220524130709'),
('20220818075833');

View file

@ -23,7 +23,8 @@ module Serializers
created_at: obj.created_at, updated_at: obj.updated_at, created_at: obj.created_at, updated_at: obj.updated_at,
due_date: obj.due_date, currency: obj.currency, due_date: obj.due_date, currency: obj.currency,
seller: seller, buyer: buyer, items: items, seller: seller, buyer: buyer, items: items,
recipient: obj.buyer.billing_email recipient: obj.buyer.billing_email,
monthly_invoice: obj.monthly_invoice
} }
end end
@ -54,11 +55,15 @@ module Serializers
end end
def items def items
invoice.items.map do |item| if invoice.monthly_invoice
{ description: item.description, unit: item.unit, invoice.metadata['items']
quantity: item.quantity, price: item.price, else
sum_without_vat: item.item_sum_without_vat, invoice.items.map do |item|
vat_amount: item.vat_amount, total: item.total } { description: item.description, unit: item.unit,
quantity: item.quantity, price: item.price,
sum_without_vat: item.item_sum_without_vat,
vat_amount: item.vat_amount, total: item.total }
end
end end
end end
@ -75,6 +80,7 @@ module Serializers
due_date: invoice.due_date, due_date: invoice.due_date,
total: invoice.total, total: invoice.total,
recipient: invoice.buyer.billing_email, recipient: invoice.buyer.billing_email,
monthly_invoice: invoice.monthly_invoice,
} }
end end
# rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/MethodLength

View file

@ -8,9 +8,9 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase
end end
def teardown def teardown
Setting.directo_monthly_number_min = 309901 Setting.directo_monthly_number_min = 309_901
Setting.directo_monthly_number_max = 309999 Setting.directo_monthly_number_max = 309_999
Setting.directo_monthly_number_last = 309901 Setting.directo_monthly_number_last = 309_901
end end
def test_directo_json_sends_customer_as_hash def test_directo_json_sends_customer_as_hash
@ -49,7 +49,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase
price = billing_prices(:create_one_year) price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price) activity.update!(activity_type: 'create', price: price)
Setting.directo_monthly_number_max = 30991 Setting.directo_monthly_number_max = 30_991
assert_raises 'RuntimeError' do assert_raises 'RuntimeError' do
DirectoInvoiceForwardJob.perform_now(monthly: true, dry: false) DirectoInvoiceForwardJob.perform_now(monthly: true, dry: false)

View file

@ -0,0 +1,265 @@
require 'test_helper'
class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
setup do
@user = registrars(:bestnames)
@date = Time.zone.parse('2010-08-06')
travel_to @date
ActionMailer::Base.deliveries.clear
EInvoice.provider = EInvoice::Providers::TestProvider.new
EInvoice::Providers::TestProvider.deliveries.clear
end
def teardown
Setting.directo_monthly_number_min = 309_901
Setting.directo_monthly_number_max = 309_999
Setting.directo_monthly_number_last = 309_901
EInvoice.provider = EInvoice::Providers::TestProvider.new
EInvoice::Providers::TestProvider.deliveries.clear
end
def test_fails_if_directo_bounds_exceedable
activity = account_activities(:one)
price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price)
Setting.directo_monthly_number_max = 30_991
assert_no_difference 'Directo.count' do
assert_raises 'RuntimeError' do
SendMonthlyInvoicesJob.perform_now
end
end
assert_nil Invoice.find_by_monthly_invoice(true)
assert_emails 0
assert_equal 0, EInvoice::Providers::TestProvider.deliveries.count
end
def test_monthly_summary_is_not_delivered_if_dry
activity = account_activities(:one)
price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price)
@user.update(language: 'et')
assert_difference 'Setting.directo_monthly_number_last' do
assert_no_difference 'Directo.count' do
SendMonthlyInvoicesJob.perform_now(dry: true)
end
end
invoice = Invoice.last
assert_equal 309_902, invoice.number
refute invoice.in_directo
assert invoice.e_invoice_sent_at.blank?
assert_emails 0
assert_equal 0, EInvoice::Providers::TestProvider.deliveries.count
end
def test_monthly_summary_is_delivered_if_invoice_already_exists
@monthly_invoice = invoices(:one)
@monthly_invoice.update(number: 309_902, monthly_invoice: true,
issue_date: @date.last_month.end_of_month,
due_date: @date.last_month.end_of_month,
metadata: metadata,
in_directo: false,
e_invoice_sent_at: nil)
activity = account_activities(:one)
price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price)
@user.update(language: 'et')
response = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<Result Type="0" Desc="OK" docid="309902" doctype="ARVE" submit="Invoices"/>
</results>
XML
stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
(body.include? '.test registreerimine: 1 aasta(t)') &&
(body.include? 'Domeenide ettemaks') &&
(body.include? '309902')
end.to_return(status: 200, body: response)
assert_no_difference 'Setting.directo_monthly_number_last' do
assert_difference('Directo.count', 1) do
SendMonthlyInvoicesJob.perform_now
end
end
invoice = Invoice.last
assert_equal 309_902, invoice.number
assert invoice.in_directo
assert_not invoice.e_invoice_sent_at.blank?
assert_emails 1
email = ActionMailer::Base.deliveries.last
assert_equal ['billing@bestnames.test'], email.to
assert_equal 'Invoice no. 309902 (monthly invoice)', email.subject
assert email.attachments['invoice-309902.pdf']
assert_equal 1, EInvoice::Providers::TestProvider.deliveries.count
end
def test_monthly_summary_is_delivered_in_estonian
activity = account_activities(:one)
price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price)
@user.update(language: 'et')
response = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<Result Type="0" Desc="OK" docid="309902" doctype="ARVE" submit="Invoices"/>
</results>
XML
stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
(body.include? '.test registreerimine: 1 aasta(t)') &&
(body.include? 'Domeenide ettemaks') &&
(body.include? '309902')
end.to_return(status: 200, body: response)
assert_difference 'Setting.directo_monthly_number_last' do
assert_difference('Directo.count', 1) do
SendMonthlyInvoicesJob.perform_now
end
end
invoice = Invoice.last
assert_equal 309_902, invoice.number
assert invoice.in_directo
assert_not invoice.e_invoice_sent_at.blank?
assert_emails 1
email = ActionMailer::Base.deliveries.last
assert_equal ['billing@bestnames.test'], email.to
assert_equal 'Invoice no. 309902 (monthly invoice)', email.subject
assert email.attachments['invoice-309902.pdf']
assert_equal 1, EInvoice::Providers::TestProvider.deliveries.count
end
def test_multi_year_purchases_have_duration_assigned
activity = account_activities(:one)
price = billing_prices(:create_one_year)
price.update(duration: 3.years)
activity.update(activity_type: 'create', price: price)
response = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<Result Type="0" Desc="OK" docid="309902" doctype="ARVE" submit="Invoices"/>
</results>
XML
stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
(body.include? 'StartDate') && (body.include? 'EndDate')
end.to_return(status: 200, body: response)
assert_difference 'Setting.directo_monthly_number_last' do
SendMonthlyInvoicesJob.perform_now
end
invoice = Invoice.last
assert_equal 309_902, invoice.number
assert invoice.in_directo
assert_not invoice.e_invoice_sent_at.blank?
end
def test_monthly_duration_products_are_present_in_summary
activity = account_activities(:one)
price = billing_prices(:create_one_month)
activity.update(activity_type: 'create', price: price)
response = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<Result Type="0" Desc="OK" docid="309902" doctype="ARVE" submit="Invoices"/>
</results>
XML
stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
body.include? 'month(s)'
end.to_return(status: 200, body: response)
assert_difference 'Setting.directo_monthly_number_last' do
SendMonthlyInvoicesJob.perform_now
end
invoice = Invoice.last
assert_equal 309_902, invoice.number
assert invoice.in_directo
assert_not invoice.e_invoice_sent_at.blank?
end
def test_sends_each_monthly_invoice_separately
WebMock.reset!
activity = account_activities(:one)
price = billing_prices(:create_one_year)
price.update(duration: 3.years)
activity.update(activity_type: 'create', price: price)
# Creating account activity for second action
another_activity = activity.dup
another_activity.account = accounts(:two)
AccountActivity.skip_callback(:create, :after, :update_balance)
another_activity.created_at = Time.zone.parse('2010-07-05 10:00')
another_activity.save
AccountActivity.set_callback(:create, :after, :update_balance)
response = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<Result Type="0" Desc="OK" docid="309902" doctype="ARVE" submit="Invoices"/>
</results>
XML
first_registrar_stub = stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
(body.include? 'StartDate') && (body.include? 'EndDate') && (body.include? 'bestnames')
end.to_return(status: 200, body: response)
second_registrar_stub = stub_request(:post, ENV['directo_invoice_url']).with do |request|
body = CGI.unescape(request.body)
(body.include? 'StartDate') && (body.include? 'EndDate') && (body.include? 'goodnames')
end.to_return(status: 200, body: response)
assert_difference('Invoice.count', 2) do
assert_difference('Directo.count', 2) do
SendMonthlyInvoicesJob.perform_now
end
end
assert_requested first_registrar_stub
assert_requested second_registrar_stub
assert_emails 2
assert_equal 2, EInvoice::Providers::TestProvider.deliveries.count
end
private
def metadata
{
"items" => [
{ "description" => "Domeenide registreerimine - Juuli 2010" },
{ "product_id" => nil, "quantity" => 1, "unit" => "tk", "price" => 10.0, "description" => ".test registreerimine: 1 aasta(t)" },
{ "product_id" => "ETTEM06", "description" => "Domeenide ettemaks", "quantity" => -1, "price" => 10.0, "unit" => "tk" },
],
}
end
end