diff --git a/Gemfile.lock b/Gemfile.lock
index d1e38fab5..fb51d2bf1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -18,10 +18,10 @@ GIT
GIT
remote: https://github.com/internetee/e_invoice.git
- revision: 312cac173935f434e449d1714f3497bfee9f8995
+ revision: 9f850465697a2448a31ebddb83c1be5a5a9be3d2
branch: master
specs:
- e_invoice (0.1.0)
+ e_invoice (0.1.3)
builder (~> 3.2)
nokogiri
savon
@@ -603,4 +603,4 @@ DEPENDENCIES
wkhtmltopdf-binary (~> 0.12.5.1)
BUNDLED WITH
- 2.3.16
+ 2.3.21
diff --git a/app/jobs/delete_monthly_invoices_job.rb b/app/jobs/delete_monthly_invoices_job.rb
new file mode 100644
index 000000000..daf79827a
--- /dev/null
+++ b/app/jobs/delete_monthly_invoices_job.rb
@@ -0,0 +1,10 @@
+class DeleteMonthlyInvoicesJob < ApplicationJob
+ queue_as :default
+
+ def perform
+ @month = Time.zone.now - 1.month
+ invoices = Invoice.where(monthly_invoice: true, issue_date: @month.end_of_month.to_date,
+ in_directo: false, e_invoice_sent_at: nil)
+ invoices.delete_all
+ end
+end
diff --git a/app/jobs/send_e_invoice_job.rb b/app/jobs/send_e_invoice_job.rb
index 33a2745c6..c48dccd2a 100644
--- a/app/jobs/send_e_invoice_job.rb
+++ b/app/jobs/send_e_invoice_job.rb
@@ -16,8 +16,9 @@ class SendEInvoiceJob < ApplicationJob
def need_to_process_invoice?(invoice:, payable:)
logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}"
+ unprocessable = invoice.do_not_send_e_invoice? && (invoice.monthly_invoice ? true : payable)
return false if invoice.blank?
- return false if invoice.do_not_send_e_invoice? && payable
+ return false if unprocessable
true
end
diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb
new file mode 100644
index 000000000..0684dc992
--- /dev/null
+++ b/app/jobs/send_monthly_invoices_job.rb
@@ -0,0 +1,147 @@
+class SendMonthlyInvoicesJob < ApplicationJob # rubocop:disable Metrics/ClassLength
+ queue_as :default
+
+ def perform(dry: false)
+ @dry = dry
+ @month = Time.zone.now - 1.month
+ @directo_client = new_directo_client
+ @min_directo_num = Setting.directo_monthly_number_min.presence.try(:to_i)
+ @max_directo_num = Setting.directo_monthly_number_max.presence.try(:to_i)
+
+ send_monthly_invoices
+ end
+
+ def new_directo_client
+ DirectoApi::Client.new(ENV['directo_invoice_url'], Setting.directo_sales_agent,
+ Setting.directo_receipt_payment_term)
+ end
+
+ # rubocop:disable Metrics/MethodLength
+ def send_monthly_invoices
+ Registrar.with_cash_accounts.find_each do |registrar|
+ summary = registrar.monthly_summary(month: @month)
+ next if summary.nil?
+
+ invoice = registrar.monthly_invoice(month: @month) || create_invoice(summary, registrar)
+ next if invoice.nil? || @dry
+
+ send_email_to_registrar(invoice: invoice, registrar: registrar)
+ send_e_invoice(invoice.id)
+ next if invoice.in_directo
+
+ Rails.logger.info("[DIRECTO] Trying to send monthly invoice #{invoice.number}")
+ @directo_client = new_directo_client
+ directo_invoices = @directo_client.invoices.add_with_schema(invoice: summary,
+ schema: 'summary')
+ next unless directo_invoices.size.positive?
+
+ directo_invoices.last.number = invoice.number
+ sync_with_directo
+ end
+ end
+
+ # rubocop:enable Metrics/MethodLength
+
+ def send_email_to_registrar(invoice:, registrar:)
+ InvoiceMailer.invoice_email(invoice: invoice,
+ recipient: registrar.billing_email)
+ .deliver_now
+ end
+
+ def send_e_invoice(invoice_id)
+ SendEInvoiceJob.set(wait: 1.minute).perform_later(invoice_id, payable: false)
+ end
+
+ def create_invoice(summary, registrar)
+ invoice = registrar.init_monthly_invoice(normalize(summary))
+ invoice.number = assign_monthly_number
+ return unless invoice.save!
+
+ update_monthly_invoice_number(num: invoice.number)
+ invoice
+ end
+
+ def sync_with_directo
+ invoices_xml = @directo_client.invoices.as_xml
+
+ Rails.logger.info("[Directo] - attempting to send following XML:\n #{invoices_xml}")
+
+ res = @directo_client.invoices.deliver(ssl_verify: false)
+ process_directo_response(res.body, invoices_xml)
+ rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
+ EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError
+ Rails.logger.info('[Directo] Failed to communicate via API')
+ end
+
+ def assign_monthly_number
+ last_directo_num = [Setting.directo_monthly_number_last.presence.try(:to_i),
+ @min_directo_num].compact.max || 0
+ raise 'Directo Counter is out of period!' if directo_counter_exceedable?(1, last_directo_num)
+
+ last_directo_num + 1
+ end
+
+ def directo_counter_exceedable?(invoices_count, last_directo_num)
+ return true if @max_directo_num && @max_directo_num < (last_directo_num + invoices_count)
+
+ false
+ end
+
+ def process_directo_response(body, req)
+ Rails.logger.info "[Directo] - Responded with body: #{body}"
+ Nokogiri::XML(body).css('Result').each do |res|
+ inv = Invoice.find_by(number: res.attributes['docid'].value.to_i)
+ mark_invoice_as_sent_to_directo(res: res, req: req, invoice: inv)
+ end
+ end
+
+ def mark_invoice_as_sent_to_directo(res:, req:, invoice: nil)
+ directo_record = Directo.new(response: res.as_json.to_h,
+ request: req, invoice_number: res.attributes['docid'].value.to_i)
+ directo_record.item = invoice
+ invoice.update(in_directo: true)
+
+ directo_record.save!
+ end
+
+ def update_monthly_invoice_number(num:)
+ return unless num.to_i > Setting.directo_monthly_number_last.to_i
+
+ Setting.directo_monthly_number_last = num.to_i
+ end
+
+ private
+
+ # rubocop:disable Metrics/MethodLength
+ def normalize(summary, lines: [])
+ sum = summary.dup
+ line_map = Hash.new 0
+ sum['invoice_lines'].each { |l| line_map[l] += 1 }
+
+ line_map.each_key do |count|
+ count['quantity'] = line_map[count] unless count['unit'].nil?
+ regex = /Domeenide ettemaks|Domains prepayment/
+ count['quantity'] = -1 if count['description'].match?(regex)
+ lines << count
+ end
+
+ sum['invoice_lines'] = summarize_lines(lines)
+ sum
+ end
+ # rubocop:enable Metrics/MethodLength
+
+ def summarize_lines(invoice_lines, lines: [])
+ line_map = Hash.new 0
+ invoice_lines.each do |l|
+ hash = l.with_indifferent_access.except(:start_date, :end_date)
+ line_map[hash] += 1
+ end
+
+ line_map.each_key do |count|
+ count['price'] = (line_map[count] * count['price'].to_f).round(3) unless count['price'].nil?
+ lines << count
+ end
+
+ lines
+ end
+end
diff --git a/app/mailers/invoice_mailer.rb b/app/mailers/invoice_mailer.rb
index a9d544d63..95b7fefd6 100644
--- a/app/mailers/invoice_mailer.rb
+++ b/app/mailers/invoice_mailer.rb
@@ -4,6 +4,7 @@ class InvoiceMailer < ApplicationMailer
subject = default_i18n_subject(invoice_number: invoice.number)
subject << I18n.t('invoice.already_paid') if paid
+ subject << I18n.t('invoice.monthly_invoice') if invoice.monthly_invoice
attachments["invoice-#{invoice.number}.pdf"] = invoice.as_pdf
mail(to: recipient, subject: subject)
end
diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb
index 76535ce12..fc1defe9a 100644
--- a/app/models/concerns/registrar/book_keeping.rb
+++ b/app/models/concerns/registrar/book_keeping.rb
@@ -1,22 +1,27 @@
-module Registrar::BookKeeping
+module Registrar::BookKeeping # rubocop:disable Metrics/ModuleLength
extend ActiveSupport::Concern
DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI',
'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze
+ included do
+ scope :with_cash_accounts, (lambda do
+ joins(:accounts).where('accounts.account_type = ? AND test_registrar != ?',
+ Account::CASH,
+ true)
+ end)
+ end
+
def monthly_summary(month:)
activities = monthly_activites(month)
return unless activities.any?
invoice = {
- 'number': 1,
- 'customer': compose_directo_customer,
+ 'number': 1, 'customer': compose_directo_customer,
'language': language == 'en' ? 'ENG' : '', 'currency': activities.first.currency,
'date': month.end_of_month.strftime('%Y-%m-%d')
}.as_json
-
invoice['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities)
-
invoice
end
@@ -55,20 +60,25 @@ module Registrar::BookKeeping
.where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW])
end
+ def monthly_invoice(month:)
+ invoices.where(monthly_invoice: true, issue_date: month.end_of_month.to_date,
+ cancelled_at: nil).first
+ end
+
def new_monthly_invoice_line(activity:, duration: nil)
price = load_price(activity)
line = {
'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym],
'quantity': 1,
'unit': language == 'en' ? 'pc' : 'tk',
- }
+ }.with_indifferent_access
finalize_invoice_line(line, price: price, duration: duration, activity: activity)
end
def finalize_invoice_line(line, price:, activity:, duration:)
yearly = price.duration.in_years.to_i >= 1
- line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i) : price.price.amount
+ line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i).to_f : price.price.amount.to_f
line['description'] = description_in_language(price: price, yearly: yearly)
add_product_timeframe(line: line, activity: activity, duration: duration) if duration.present? && (duration > 1)
@@ -79,15 +89,16 @@ module Registrar::BookKeeping
def add_product_timeframe(line:, activity:, duration:)
create_time = activity.created_at
line['start_date'] = (create_time + (duration - 1).year).end_of_month.strftime('%Y-%m-%d')
- line['end_date'] = (create_time + (duration - 1).year + 1).end_of_month.strftime('%Y-%m-%d')
+ line['end_date'] = (create_time + duration.year).end_of_month.strftime('%Y-%m-%d')
end
def description_in_language(price:, yearly:)
timeframe_string = yearly ? 'yearly' : 'monthly'
locale_string = "registrar.invoice_#{timeframe_string}_product_description"
+ length = yearly ? price.duration.in_years.to_i : price.duration.in_months.to_i
I18n.with_locale(language == 'en' ? 'en' : 'et') do
- I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.in_years.to_i)
+ I18n.t(locale_string, tld: ".#{price.zone_name}", length: length)
end
end
diff --git a/app/models/invoice.rb b/app/models/invoice.rb
index b7e60abfb..327a107cd 100644
--- a/app/models/invoice.rb
+++ b/app/models/invoice.rb
@@ -32,11 +32,14 @@ class Invoice < ApplicationRecord
# rubocop:enable Layout/LineLength
# rubocop:enable Style/MultilineBlockLayout
validates :due_date, :currency, :seller_name,
- :seller_iban, :buyer_name, :items, presence: true
+ :seller_iban, :buyer_name, presence: true
+ validates :items, presence: true, unless: -> { monthly_invoice }
before_create :set_invoice_number
before_create :calculate_total, unless: :total?
before_create :apply_default_buyer_vat_no, unless: :buyer_vat_no?
+ skip_callback :create, :before, :set_invoice_number, if: -> { monthly_invoice }
+ skip_callback :create, :before, :calculate_total, if: -> { monthly_invoice }
attribute :vat_rate, ::Type::VatRate.new
@@ -118,7 +121,7 @@ class Invoice < ApplicationRecord
end
def subtotal
- items.map(&:item_sum_without_vat).reduce(:+)
+ items.map(&:item_sum_without_vat).reduce(:+) || 0
end
def vat_amount
@@ -131,7 +134,11 @@ class Invoice < ApplicationRecord
end
def each(&block)
- items.each(&block)
+ if monthly_invoice
+ metadata['items'].map { |el| OpenStruct.new(el) }.each(&block)
+ else
+ items.each(&block)
+ end
end
def as_pdf
diff --git a/app/models/invoice/e_invoice_generator.rb b/app/models/invoice/e_invoice_generator.rb
index 2361656a7..8d9675475 100644
--- a/app/models/invoice/e_invoice_generator.rb
+++ b/app/models/invoice/e_invoice_generator.rb
@@ -41,22 +41,16 @@ class Invoice
e_invoice_invoice_items = []
invoice.each do |invoice_item|
- e_invoice_invoice_item = EInvoice::InvoiceItem.new.tap do |i|
- i.description = invoice_item.description
- i.price = invoice_item.price
- i.quantity = invoice_item.quantity
- i.unit = invoice_item.unit
- i.subtotal = invoice_item.subtotal
- i.vat_rate = invoice_item.vat_rate
- i.vat_amount = invoice_item.vat_amount
- i.total = invoice_item.total
- end
+ e_invoice_invoice_item = generate_invoice_item(invoice, invoice_item)
e_invoice_invoice_items << e_invoice_invoice_item
end
+ e_invoice_name_item = e_invoice_invoice_items.shift if invoice.monthly_invoice
+
e_invoice_invoice = EInvoice::Invoice.new.tap do |i|
i.seller = seller
i.buyer = buyer
+ i.name = e_invoice_name_item&.description
i.items = e_invoice_invoice_items
i.number = invoice.number
i.date = invoice.issue_date
@@ -72,9 +66,33 @@ class Invoice
i.currency = invoice.currency
i.delivery_channel = %i[internet_bank portal]
i.payable = payable
+ i.monthly_invoice = invoice.monthly_invoice
end
EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice)
end
+
+ private
+
+ def generate_invoice_item(invoice, item)
+ EInvoice::InvoiceItem.new.tap do |i|
+ i.description = item.description
+ i.unit = item.unit
+ i.price = item.price
+ i.quantity = item.quantity
+ if invoice.monthly_invoice && item.price && item.quantity
+ i.product_id = item.product_id
+ i.vat_rate = invoice.vat_rate
+ i.subtotal = (item.price * item.quantity).round(3)
+ i.vat_amount = i.subtotal * (i.vat_rate / 100)
+ i.total = i.subtotal + i.vat_amount
+ else
+ i.subtotal = item.subtotal
+ i.vat_rate = item.vat_rate
+ i.vat_amount = item.vat_amount
+ i.total = item.total
+ end
+ end
+ end
end
end
diff --git a/app/models/invoice/pdf_generator.rb b/app/models/invoice/pdf_generator.rb
index 14fb99814..7762456c4 100644
--- a/app/models/invoice/pdf_generator.rb
+++ b/app/models/invoice/pdf_generator.rb
@@ -14,7 +14,8 @@ class Invoice
private
def invoice_html
- ApplicationController.render(template: 'invoice/pdf', assigns: { invoice: invoice })
+ template = invoice.monthly_invoice ? 'invoice/monthly_pdf' : 'invoice/pdf'
+ ApplicationController.render(template: template, assigns: { invoice: invoice })
end
end
end
diff --git a/app/models/registrar.rb b/app/models/registrar.rb
index 4c0098de0..1dbd2061d 100644
--- a/app/models/registrar.rb
+++ b/app/models/registrar.rb
@@ -1,4 +1,4 @@
-class Registrar < ApplicationRecord
+class Registrar < ApplicationRecord # rubocop:disable Metrics/ClassLength
include Versions # version/registrar_version.rb
include Registrar::BookKeeping
include EmailVerifable
@@ -56,9 +56,48 @@ class Registrar < ApplicationRecord
end
end
- def issue_prepayment_invoice(amount, description = nil, payable: true)
- vat_rate = ::Invoice::VatRateCalculator.new(registrar: self).calculate
+ # rubocop:disable Metrics/MethodLength
+ def init_monthly_invoice(summary)
+ Invoice.new(
+ issue_date: summary['date'].to_date,
+ due_date: summary['date'].to_date,
+ currency: 'EUR',
+ description: I18n.t('invoice.monthly_invoice_description'),
+ seller_name: Setting.registry_juridical_name,
+ seller_reg_no: Setting.registry_reg_no,
+ seller_iban: Setting.registry_iban,
+ seller_bank: Setting.registry_bank,
+ seller_swift: Setting.registry_swift,
+ seller_vat_no: Setting.registry_vat_no,
+ seller_country_code: Setting.registry_country_code,
+ seller_state: Setting.registry_state,
+ seller_street: Setting.registry_street,
+ seller_city: Setting.registry_city,
+ seller_zip: Setting.registry_zip,
+ seller_phone: Setting.registry_phone,
+ seller_url: Setting.registry_url,
+ seller_email: Setting.registry_email,
+ seller_contact_name: Setting.registry_invoice_contact,
+ buyer: self,
+ buyer_name: name,
+ buyer_reg_no: reg_no,
+ buyer_country_code: address_country_code,
+ buyer_state: address_state,
+ buyer_street: address_street,
+ buyer_city: address_city,
+ buyer_zip: address_zip,
+ buyer_phone: phone,
+ buyer_url: website,
+ buyer_email: email,
+ reference_no: reference_no,
+ vat_rate: calculate_vat_rate,
+ monthly_invoice: true,
+ metadata: { items: summary['invoice_lines'] },
+ total: 0
+ )
+ end
+ def issue_prepayment_invoice(amount, description = nil, payable: true)
invoice = invoices.create!(
issue_date: Time.zone.today,
due_date: (Time.zone.now + Setting.days_to_keep_invoices_active.days).to_date,
@@ -91,7 +130,7 @@ class Registrar < ApplicationRecord
buyer_url: website,
buyer_email: email,
reference_no: reference_no,
- vat_rate: vat_rate,
+ vat_rate: calculate_vat_rate,
items_attributes: [
{
description: 'prepayment',
@@ -124,6 +163,7 @@ class Registrar < ApplicationRecord
invoice
end
+ # rubocop:enable Metrics/MethodLength
def cash_account
accounts.find_by(account_type: Account::CASH)
@@ -265,4 +305,8 @@ class Registrar < ApplicationRecord
def vat_liable_in_foreign_country?
!vat_liable_locally?
end
+
+ def calculate_vat_rate
+ ::Invoice::VatRateCalculator.new(registrar: self).calculate
+ end
end
diff --git a/app/views/admin/invoices/show.haml b/app/views/admin/invoices/show.haml
index b121c8337..42f7d769c 100644
--- a/app/views/admin/invoices/show.haml
+++ b/app/views/admin/invoices/show.haml
@@ -4,11 +4,12 @@
= @invoice
.col-sm-8
%h1.text-right.text-center-xs
- - if @invoice.unpaid?
- = link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default')
+ - unless @invoice.monthly_invoice
+ - if @invoice.unpaid?
+ = link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default')
- - if @invoice.paid? and !@invoice.cancelled?
- = link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning')
+ - if @invoice.paid? && !@invoice.cancelled?
+ = link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning')
= link_to(t('.download_btn'), download_admin_invoice_path(@invoice), class: 'btn btn-default')
= link_to(t('.deliver_btn'), new_admin_invoice_delivery_path(@invoice), class: 'btn btn-default')
@@ -24,6 +25,9 @@
.col-md-6= render 'registrar/invoices/partials/seller'
.col-md-6= render 'registrar/invoices/partials/buyer'
.row
- .col-md-12= render 'registrar/invoices/partials/items'
+ - if @invoice.monthly_invoice
+ .col-md-12= render 'registrar/invoices/partials/monthly_invoice_items'
+ - else
+ .col-md-12= render 'registrar/invoices/partials/items'
.row
.col-md-12= render 'registrar/invoices/partials/payment_orders'
diff --git a/app/views/invoice/monthly_pdf.haml b/app/views/invoice/monthly_pdf.haml
new file mode 100644
index 000000000..c7e179d03
--- /dev/null
+++ b/app/views/invoice/monthly_pdf.haml
@@ -0,0 +1,277 @@
+%html{lang: I18n.locale.to_s}
+ %head
+ %meta{charset: "utf-8"}
+ :css
+ .container {
+ margin: auto;
+ font-size: 12px;
+ }
+
+ .col-md-12 {
+
+ }
+
+ .col-md-6 {
+ width: 49%;
+ display: inline-block;
+ }
+
+ .col-xs-4 {
+ width: 33%;
+ }
+
+ .col-xs-2 {
+ width: 16%;
+ }
+
+ .col-md-3 {
+ width: 24%;
+ display: inline-block;
+ }
+
+ .left {
+ float: left;
+ }
+
+ .left {
+ padding-right: 5px;
+ }
+
+ .right {
+ float: right;
+ }
+
+ .text-right {
+ text-align: right;
+ }
+
+ dt {
+ float: left;
+ width: 100px;
+ clear: left;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: bold;
+ line-height: 1.42857;
+ }
+
+ dd {
+ margin-left: 120px;
+ line-height: 1.42857;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+ }
+
+ th {
+ text-align: left;
+ border: 0px;
+ border-top: 1px solid #DDD;
+ padding: 6px;
+ }
+
+ thead th {
+ border-bottom: 2px solid #DDD;
+ border-top: 0px;
+ }
+
+ td {
+ border-top: 1px solid #DDD;
+ }
+
+ td {
+ padding: 6px;
+ }
+
+ .no-border {
+ border: 0px;
+ }
+
+ hr {
+ height: 1px;
+ border: 0;
+ color: #DDD;
+ background-color: #DDD;
+ }
+
+ .clear {
+ clear: both;
+ }
+
+ .pull-down {
+ margin-top: 50px;
+ }
+
+ #header {
+ position: relative;
+ min-height: 100px;
+ }
+
+ img {
+ width: 106px;
+ height: 102px;
+ }
+
+ #header-content {
+ position: absolute;
+ bottom: 0;
+ }
+
+ #footer {
+ position: absolute;
+ bottom: 0px;
+ width: 99%;
+ }
+
+ h1 {
+ margin-bottom: 5px;
+ }
+ %body
+ .container
+ #header.row
+ .col-sm-6.left
+ #header-content
+ %h1
+ = @invoice
+ .col-sm-6.right
+ %img{src: "#{Rails.root}/public/eis-logo-black-et.png"}
+ .clear
+ %hr
+ .row
+ .col-md-6.left
+ %h4
+ Details
+ %hr
+ %dl.dl-horizontal
+ %dt= t(:issue_date)
+ %dd= l @invoice.issue_date
+
+ - if @invoice.cancelled?
+ %dt= Invoice.human_attribute_name :cancelled_at
+ %dd= l @invoice.cancelled_at
+
+ %dt= t(:due_date)
+ - if @invoice.cancelled?
+ %dd= t(:cancelled)
+ - else
+ %dd= l @invoice.due_date
+
+ %dt= t(:issuer)
+ %dd= @invoice.seller_contact_name
+
+ - if @invoice.description.present?
+ %dt= t(:description)
+ %dd=@invoice.description
+
+ %dt= Invoice.human_attribute_name :reference_no
+ %dd= @invoice.reference_no
+
+ .col-md-6.right
+ %h4= t(:client)
+ %hr
+ %dl.dl-horizontal
+ %dt= t(:name)
+ %dd= @invoice.buyer_name
+
+ %dt= t(:reg_no)
+ %dd= @invoice.buyer_reg_no
+
+ - if @invoice.buyer_address.present?
+ %dt= Invoice.human_attribute_name :address
+ %dd= @invoice.buyer_address
+
+ - if @invoice.buyer_country.present?
+ %dt= t(:country)
+ %dd= @invoice.buyer_country
+
+ - if @invoice.buyer_phone.present?
+ %dt= t(:phone)
+ %dd= @invoice.buyer_phone
+
+ - if @invoice.buyer_url.present?
+ %dt= t(:url)
+ %dd= @invoice.buyer_url
+
+ - if @invoice.buyer_email.present?
+ %dt= t(:email)
+ %dd= @invoice.buyer_email
+
+ .clear
+ .row.pull-down
+ .col-md-12
+ .table-responsive
+ %table.table.table-hover.table-condensed
+ %thead
+ %tr
+ %th{class: 'col-xs-1'}= t(:code)
+ %th{class: 'col-xs-1'}= InvoiceItem.human_attribute_name :quantity
+ %th{class: 'col-xs-1'}= t(:unit)
+ %th{class: 'col-xs-5'}= t(:description)
+ %th{class: 'col-xs-2'}= t(:price)
+ %th{class: 'col-xs-2'}= t(:total)
+ %tbody
+ - @invoice.each do |invoice_item|
+ %tr
+ %td= invoice_item.product_id
+ %td= invoice_item.quantity
+ %td= invoice_item.unit
+ %td= invoice_item.description
+ - if invoice_item.price && invoice_item.quantity
+ %td= currency(invoice_item.price)
+ %td= "#{currency((invoice_item.price * invoice_item.quantity).round(3))} #{@invoice.currency}"
+ - else
+ %td= ''
+ %td= ''
+ %tfoot
+ %tr
+ %th{colspan: 4}
+ %th= Invoice.human_attribute_name :subtotal
+ %td= number_to_currency(0)
+ %tr
+ %th.no-border{colspan: 4}
+ %th= "VAT #{number_to_percentage(@invoice.vat_rate, precision: 1)}"
+ %td= number_to_currency(0)
+ %tr
+ %th.no-border{colspan: 4}
+ %th= t(:total)
+ %td= number_to_currency(0)
+
+ #footer
+ %hr
+ .row
+ .col-md-3.left
+ = @invoice.seller_name
+ %br
+ = @invoice.seller_address
+ %br
+ = @invoice.seller_country
+ %br
+ = "#{t('reg_no')} #{@invoice.seller_reg_no}"
+ %br
+ = "#{Registrar.human_attribute_name :vat_no} #{@invoice.seller_vat_no}"
+
+ .col-md-3.left
+ = @invoice.seller_phone
+ %br
+ = @invoice.seller_email
+ %br
+ = @invoice.seller_url
+
+ .col-md-3.text-right.left
+ = t(:bank)
+ %br
+ = t(:iban)
+ %br
+ = t(:swift)
+
+ .col-md-3.left
+ = @invoice.seller_bank
+ %br
+ = @invoice.seller_iban
+ %br
+ = @invoice.seller_swift
\ No newline at end of file
diff --git a/app/views/invoice/pdf.haml b/app/views/invoice/pdf.haml
index 9f10acdad..dc3d4370a 100644
--- a/app/views/invoice/pdf.haml
+++ b/app/views/invoice/pdf.haml
@@ -234,7 +234,7 @@
%td= invoice_item.unit
%td= invoice_item.quantity
%td= currency(invoice_item.price)
- %td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}"
+ %td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}"
%tfoot
%tr
%th{colspan: 3}
diff --git a/app/views/registrar/invoices/partials/_monthly_invoice_items.haml b/app/views/registrar/invoices/partials/_monthly_invoice_items.haml
new file mode 100644
index 000000000..787218ea3
--- /dev/null
+++ b/app/views/registrar/invoices/partials/_monthly_invoice_items.haml
@@ -0,0 +1,38 @@
+%h4= t(:items)
+%hr
+.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)
\ No newline at end of file
diff --git a/app/views/registrar/invoices/show.haml b/app/views/registrar/invoices/show.haml
index 5e6104091..dd19a0bea 100644
--- a/app/views/registrar/invoices/show.haml
+++ b/app/views/registrar/invoices/show.haml
@@ -13,7 +13,10 @@
.col-md-6= render 'registrar/invoices/partials/seller'
.col-md-6= render 'registrar/invoices/partials/buyer'
.row
- .col-md-12= render 'registrar/invoices/partials/items'
+ - if @invoice.monthly_invoice
+ .col-md-12= render 'registrar/invoices/partials/monthly_invoice_items'
+ - else
+ .col-md-12= render 'registrar/invoices/partials/items'
- if @invoice.payable?
.row.semifooter
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ec8953a84..9021eff60 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -484,6 +484,8 @@ en:
invoice:
title: 'Invoice'
already_paid: " (already paid)"
+ monthly_invoice: " (monthly invoice)"
+ monthly_invoice_description: 'Monthly invoice'
bank_statements: 'Bank statements'
back_to_bank_statements: 'Back to bank statements'
back_to_bank_statement: 'Back to bank statement'
diff --git a/config/locales/et.yml b/config/locales/et.yml
index ad04db007..84672ed3c 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -9,3 +9,5 @@ et:
invoice:
title: 'Arve'
already_paid: " (juba makstud)"
+ monthly_invoice: " (kuuaruanne)"
+ monthly_invoice_description: 'Kuuaruanne'
diff --git a/db/migrate/20220818075833_add_monthly_invoice_type_columns.rb b/db/migrate/20220818075833_add_monthly_invoice_type_columns.rb
new file mode 100644
index 000000000..33c98124c
--- /dev/null
+++ b/db/migrate/20220818075833_add_monthly_invoice_type_columns.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index a96474d53..6e4c145f1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -955,14 +955,14 @@ CREATE TABLE public.domains (
pending_json jsonb,
force_delete_date date,
statuses character varying[],
+ status_notes public.hstore,
upid integer,
up_date timestamp without time zone,
uuid uuid DEFAULT public.gen_random_uuid() NOT NULL,
locked_by_registrant_at timestamp without time zone,
force_delete_start timestamp without time zone,
force_delete_data public.hstore,
- json_statuses_history jsonb,
- status_notes public.hstore
+ json_statuses_history jsonb
);
@@ -985,6 +985,98 @@ CREATE SEQUENCE public.domains_id_seq
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: -
--
@@ -1104,6 +1196,8 @@ CREATE TABLE public.invoices (
issue_date date NOT NULL,
e_invoice_sent_at timestamp without time zone,
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))
);
@@ -2190,6 +2284,74 @@ CREATE SEQUENCE public.payment_orders_id_seq
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: -
--
@@ -2228,6 +2390,48 @@ CREATE SEQUENCE public.prices_id_seq
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: -
--
@@ -2508,8 +2712,7 @@ CREATE TABLE public.validation_events (
validation_eventable_type character varying,
validation_eventable_id bigint,
created_at timestamp(6) without time zone NOT NULL,
- updated_at timestamp(6) without time zone NOT NULL,
- event_type public.validation_type
+ updated_at timestamp(6) without time zone NOT NULL
);
@@ -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);
+--
+-- 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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -3274,6 +3519,30 @@ ALTER TABLE ONLY public.domains
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: -
--
@@ -3522,6 +3791,22 @@ ALTER TABLE ONLY public.payment_orders
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: -
--
@@ -3530,6 +3815,14 @@ ALTER TABLE ONLY public.prices
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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -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);
---
--- 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: -
--
@@ -5085,9 +5392,11 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210708131814'),
('20210729131100'),
('20210729134625'),
-('20210827185249'),
-('20211029073644'),
+('20211028122103'),
+('20211028125245'),
+('20211029082225'),
('20211124071418'),
+('20211124084308'),
('20211125181033'),
('20211125184334'),
('20211126085139'),
@@ -5098,12 +5407,12 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220124105717'),
('20220228093211'),
('20220316140727'),
-('20220406085500'),
('20220412130856'),
('20220413073315'),
('20220413084536'),
('20220413084748'),
('20220504090512'),
-('20220524130709');
+('20220524130709'),
+('20220818075833');
diff --git a/lib/serializers/repp/invoice.rb b/lib/serializers/repp/invoice.rb
index 3b8efbd58..686eaac99 100644
--- a/lib/serializers/repp/invoice.rb
+++ b/lib/serializers/repp/invoice.rb
@@ -23,7 +23,8 @@ module Serializers
created_at: obj.created_at, updated_at: obj.updated_at,
due_date: obj.due_date, currency: obj.currency,
seller: seller, buyer: buyer, items: items,
- recipient: obj.buyer.billing_email
+ recipient: obj.buyer.billing_email,
+ monthly_invoice: obj.monthly_invoice
}
end
@@ -54,11 +55,15 @@ module Serializers
end
def items
- invoice.items.map do |item|
- { 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 }
+ if invoice.monthly_invoice
+ invoice.metadata['items']
+ else
+ invoice.items.map do |item|
+ { 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
@@ -75,6 +80,7 @@ module Serializers
due_date: invoice.due_date,
total: invoice.total,
recipient: invoice.buyer.billing_email,
+ monthly_invoice: invoice.monthly_invoice,
}
end
# rubocop:enable Metrics/MethodLength
diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb
index 33a05f644..11ed657f6 100644
--- a/test/jobs/directo_invoice_forward_job_test.rb
+++ b/test/jobs/directo_invoice_forward_job_test.rb
@@ -8,9 +8,9 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase
end
def teardown
- Setting.directo_monthly_number_min = 309901
- Setting.directo_monthly_number_max = 309999
- Setting.directo_monthly_number_last = 309901
+ Setting.directo_monthly_number_min = 309_901
+ Setting.directo_monthly_number_max = 309_999
+ Setting.directo_monthly_number_last = 309_901
end
def test_directo_json_sends_customer_as_hash
@@ -49,7 +49,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase
price = billing_prices(:create_one_year)
activity.update!(activity_type: 'create', price: price)
- Setting.directo_monthly_number_max = 30991
+ Setting.directo_monthly_number_max = 30_991
assert_raises 'RuntimeError' do
DirectoInvoiceForwardJob.perform_now(monthly: true, dry: false)
diff --git a/test/jobs/send_monthly_invoices_job_test.rb b/test/jobs/send_monthly_invoices_job_test.rb
new file mode 100644
index 000000000..9417278d7
--- /dev/null
+++ b/test/jobs/send_monthly_invoices_job_test.rb
@@ -0,0 +1,275 @@
+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
+
+ 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
+
+ perform_enqueued_jobs
+
+ 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_month)
+ activity.update!(activity_type: 'create', price: price)
+ @user.update(language: 'et')
+
+ response = <<-XML
+
+
+
+
+ XML
+
+ stub_request(:post, ENV['directo_invoice_url']).with do |request|
+ body = CGI.unescape(request.body)
+
+ (body.include? '.test registreerimine: 3 kuu(d)') &&
+ (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
+
+ perform_enqueued_jobs
+
+ 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
+
+ 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
+
+ perform_enqueued_jobs
+
+ 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
+
+ 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
+
+ perform_enqueued_jobs
+
+ 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
+
+ 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
+
+ perform_enqueued_jobs
+
+ 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
\ No newline at end of file