From d589aa16811f258b13d7d6e44991a6cbb2fdeca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Sun, 21 Aug 2022 19:11:39 +0300 Subject: [PATCH 01/10] Created job for sending monthly invoices --- app/jobs/delete_monthly_invoices_job.rb | 10 + app/jobs/send_e_invoice_job.rb | 2 +- app/jobs/send_monthly_invoices_job.rb | 155 ++++++++ app/mailers/invoice_mailer.rb | 1 + app/models/concerns/registrar/book_keeping.rb | 7 +- app/models/invoice.rb | 13 +- app/models/invoice/e_invoice_generator.rb | 27 +- app/models/invoice/pdf_generator.rb | 3 +- app/views/admin/invoices/show.haml | 9 +- app/views/invoice/monthly_pdf.haml | 277 ++++++++++++++ app/views/invoice/pdf.haml | 2 +- config/locales/en.yml | 2 + config/locales/et.yml | 2 + ...075833_add_monthly_invoice_type_columns.rb | 6 + db/structure.sql | 339 +++++++++++++++++- lib/serializers/repp/invoice.rb | 18 +- test/jobs/directo_invoice_forward_job_test.rb | 8 +- test/jobs/send_monthly_invoices_job_test.rb | 265 ++++++++++++++ 18 files changed, 1103 insertions(+), 43 deletions(-) create mode 100644 app/jobs/delete_monthly_invoices_job.rb create mode 100644 app/jobs/send_monthly_invoices_job.rb create mode 100644 app/views/invoice/monthly_pdf.haml create mode 100644 db/migrate/20220818075833_add_monthly_invoice_type_columns.rb create mode 100644 test/jobs/send_monthly_invoices_job_test.rb diff --git a/app/jobs/delete_monthly_invoices_job.rb b/app/jobs/delete_monthly_invoices_job.rb new file mode 100644 index 000000000..8a38d0d14 --- /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 \ No newline at end of file diff --git a/app/jobs/send_e_invoice_job.rb b/app/jobs/send_e_invoice_job.rb index 33a2745c6..4e77926b0 100644 --- a/app/jobs/send_e_invoice_job.rb +++ b/app/jobs/send_e_invoice_job.rb @@ -17,7 +17,7 @@ class SendEInvoiceJob < ApplicationJob def need_to_process_invoice?(invoice:, payable:) logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}" 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 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..ddb4bd80c --- /dev/null +++ b/app/jobs/send_monthly_invoices_job.rb @@ -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 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..4431df9b8 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -55,6 +55,11 @@ 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 = { @@ -68,7 +73,7 @@ module Registrar::BookKeeping 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) 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..28373b1fb 100644 --- a/app/models/invoice/e_invoice_generator.rb +++ b/app/models/invoice/e_invoice_generator.rb @@ -46,10 +46,17 @@ class Invoice 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 + if invoice.monthly_invoice + i.subtotal = 0 + i.vat_rate = 0 + 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 e_invoice_invoice_items << e_invoice_invoice_item end @@ -66,9 +73,15 @@ class Invoice i.beneficiary_name = invoice.seller_name i.beneficiary_account_number = invoice.seller_iban i.payer_name = invoice.buyer_name - i.subtotal = invoice.subtotal - i.vat_amount = invoice.vat_amount - i.total = invoice.total + if invoice.monthly_invoice + i.subtotal = 0 + 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.delivery_channel = %i[internet_bank portal] i.payable = payable 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/views/admin/invoices/show.haml b/app/views/admin/invoices/show.haml index b121c8337..f64bd9cc4 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') 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/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..38c6252f4 --- /dev/null +++ b/test/jobs/send_monthly_invoices_job_test.rb @@ -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 + + 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 + + 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 + + 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 + + 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 + + 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 \ No newline at end of file From 72022aab8d28ee32d5ec2efa32941c13b4ed5ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Mon, 22 Aug 2022 10:23:14 +0300 Subject: [PATCH 02/10] Refactored code --- app/jobs/delete_monthly_invoices_job.rb | 2 +- app/jobs/send_monthly_invoices_job.rb | 86 +++++-------------- app/models/concerns/registrar/book_keeping.rb | 9 ++ app/models/registrar.rb | 50 ++++++++++- 4 files changed, 78 insertions(+), 69 deletions(-) diff --git a/app/jobs/delete_monthly_invoices_job.rb b/app/jobs/delete_monthly_invoices_job.rb index 8a38d0d14..daf79827a 100644 --- a/app/jobs/delete_monthly_invoices_job.rb +++ b/app/jobs/delete_monthly_invoices_job.rb @@ -7,4 +7,4 @@ class DeleteMonthlyInvoicesJob < ApplicationJob in_directo: false, e_invoice_sent_at: nil) invoices.delete_all end -end \ No newline at end of file +end diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb index ddb4bd80c..78f35ad1f 100644 --- a/app/jobs/send_monthly_invoices_job.rb +++ b/app/jobs/send_monthly_invoices_job.rb @@ -16,22 +16,17 @@ class SendMonthlyInvoicesJob < ApplicationJob Setting.directo_receipt_payment_term) end + # rubocop:disable Metrics/MethodLength def send_monthly_invoices - Registrar.where.not(test_registrar: true).find_each do |registrar| - next unless registrar.cash_account - + 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 - InvoiceMailer.invoice_email(invoice: invoice, - recipient: registrar.billing_email) - .deliver_now - - SendEInvoiceJob.set(wait: 1.minute).perform_now(invoice.id, payable: false) - + 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}") @@ -44,50 +39,24 @@ class SendMonthlyInvoicesJob < ApplicationJob 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_now(invoice_id, payable: false) + 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 - ) + invoice = registrar.init_monthly_invoice(summary) + invoice.number = assign_monthly_number return unless invoice.save! - update_directo_number(num: invoice.number) + update_monthly_invoice_number(num: invoice.number) invoice end @@ -103,19 +72,6 @@ class SendMonthlyInvoicesJob < ApplicationJob 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 @@ -134,11 +90,11 @@ class SendMonthlyInvoicesJob < ApplicationJob 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) + mark_invoice_as_sent_to_directo(res: res, req: req, invoice: inv) end end - def mark_invoice_as_sent(res:, req:, invoice: nil) + 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 @@ -147,7 +103,7 @@ class SendMonthlyInvoicesJob < ApplicationJob directo_record.save! end - def update_directo_number(num:) + 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 diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index 4431df9b8..be281fb56 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -4,6 +4,15 @@ module Registrar::BookKeeping 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? diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 4c0098de0..811fc3921 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -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 From a21e5c195423ee43250b9d16ebe4ed2b48d590c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Tue, 23 Aug 2022 09:20:39 +0300 Subject: [PATCH 03/10] Modified views for monthly invoices --- app/models/concerns/registrar/book_keeping.rb | 7 ++-- app/views/admin/invoices/show.haml | 5 ++- .../partials/_monthly_invoice_items.haml | 38 +++++++++++++++++++ app/views/registrar/invoices/show.haml | 5 ++- 4 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 app/views/registrar/invoices/partials/_monthly_invoice_items.haml diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index be281fb56..e1c980ee2 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -6,10 +6,9 @@ module Registrar::BookKeeping included do scope :with_cash_accounts, (lambda do - joins(:accounts) - .where('accounts.account_type = ? AND test_registrar != ?', - Account::CASH, - true) + joins(:accounts).where('accounts.account_type = ? AND test_registrar != ?', + Account::CASH, + true) end) end diff --git a/app/views/admin/invoices/show.haml b/app/views/admin/invoices/show.haml index f64bd9cc4..42f7d769c 100644 --- a/app/views/admin/invoices/show.haml +++ b/app/views/admin/invoices/show.haml @@ -25,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/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 From 14a9b5b7097d2f999de1683042e8bfbcbe81cdaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Mon, 29 Aug 2022 14:06:04 +0300 Subject: [PATCH 04/10] Modifications for monthly invoices --- Gemfile.lock | 6 +- app/jobs/send_e_invoice_job.rb | 3 +- app/models/concerns/registrar/book_keeping.rb | 5 +- app/models/invoice/e_invoice_generator.rb | 64 +++++++++++-------- test/jobs/send_monthly_invoices_job_test.rb | 4 +- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d1e38fab5..a6c822723 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,10 +18,10 @@ GIT GIT remote: https://github.com/internetee/e_invoice.git - revision: 312cac173935f434e449d1714f3497bfee9f8995 + revision: da18f3da3219315f732b94fbc165fe83cb828a99 branch: master specs: - e_invoice (0.1.0) + e_invoice (0.1.1) 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/send_e_invoice_job.rb b/app/jobs/send_e_invoice_job.rb index 4e77926b0..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? && (invoice.monthly_invoice ? true : payable) + return false if unprocessable true end diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index e1c980ee2..13574f3c2 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -74,7 +74,7 @@ module Registrar::BookKeeping '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 @@ -98,9 +98,10 @@ module Registrar::BookKeeping 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/e_invoice_generator.rb b/app/models/invoice/e_invoice_generator.rb index 28373b1fb..4830e486a 100644 --- a/app/models/invoice/e_invoice_generator.rb +++ b/app/models/invoice/e_invoice_generator.rb @@ -41,22 +41,10 @@ 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 - if invoice.monthly_invoice - i.subtotal = 0 - i.vat_rate = 0 - 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 + if invoice.monthly_invoice + e_invoice_invoice_item = generate_monthly_invoice_item(invoice, invoice_item) + else + e_invoice_invoice_item = generate_normal_invoice_item(invoice_item) end e_invoice_invoice_items << e_invoice_invoice_item end @@ -73,21 +61,47 @@ class Invoice i.beneficiary_name = invoice.seller_name i.beneficiary_account_number = invoice.seller_iban i.payer_name = invoice.buyer_name - if invoice.monthly_invoice - i.subtotal = 0 - i.vat_amount = 0 - i.total = 0 - else - i.subtotal = invoice.subtotal - i.vat_amount = invoice.vat_amount - i.total = invoice.total - end + i.subtotal = invoice.subtotal + i.vat_amount = invoice.vat_amount + i.total = invoice.total 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_normal_invoice_item(item) + EInvoice::InvoiceItem.new.tap do |i| + i.description = item.description + i.unit = item.unit + i.price = item.price + i.quantity = item.quantity + i.subtotal = item.subtotal + i.vat_rate = item.vat_rate + i.vat_amount = item.vat_amount + i.total = item.total + end + end + + def generate_monthly_invoice_item(invoice, item) + EInvoice::InvoiceItem.new.tap do |i| + i.description = item.description + i.description = "[#{item.product_id}] #{item.description}" if item.product_id + i.unit = item.unit + i.price = item.price + i.quantity = item.quantity + if item.price && item.quantity + i.subtotal = (item.price * item.quantity).round(3) + i.vat_rate = invoice.vat_rate + i.vat_amount = i.subtotal * (i.vat_rate / 100) + i.total = i.subtotal + i.vat_amount + end + end + end end end diff --git a/test/jobs/send_monthly_invoices_job_test.rb b/test/jobs/send_monthly_invoices_job_test.rb index 38c6252f4..95937df73 100644 --- a/test/jobs/send_monthly_invoices_job_test.rb +++ b/test/jobs/send_monthly_invoices_job_test.rb @@ -110,7 +110,7 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase def test_monthly_summary_is_delivered_in_estonian activity = account_activities(:one) - price = billing_prices(:create_one_year) + price = billing_prices(:create_one_month) activity.update!(activity_type: 'create', price: price) @user.update(language: 'et') @@ -124,7 +124,7 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase stub_request(:post, ENV['directo_invoice_url']).with do |request| body = CGI.unescape(request.body) - (body.include? '.test registreerimine: 1 aasta(t)') && + (body.include? '.test registreerimine: 3 kuu(d)') && (body.include? 'Domeenide ettemaks') && (body.include? '309902') end.to_return(status: 200, body: response) From 77a6f6d9855972c1cd23e24f39ce99ffee12d96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Mon, 29 Aug 2022 14:15:57 +0300 Subject: [PATCH 05/10] Set perform later for SendEInvoiceJob --- app/jobs/send_monthly_invoices_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb index 78f35ad1f..647bd0752 100644 --- a/app/jobs/send_monthly_invoices_job.rb +++ b/app/jobs/send_monthly_invoices_job.rb @@ -48,7 +48,7 @@ class SendMonthlyInvoicesJob < ApplicationJob end def send_e_invoice(invoice_id) - SendEInvoiceJob.set(wait: 1.minute).perform_now(invoice_id, payable: false) + SendEInvoiceJob.set(wait: 1.minute).perform_later(invoice_id, payable: false) end def create_invoice(summary, registrar) From 5d92442a48f7ed63526005bebec93e74094b0ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Mon, 29 Aug 2022 16:06:14 +0300 Subject: [PATCH 06/10] Updated test --- test/jobs/send_monthly_invoices_job_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/jobs/send_monthly_invoices_job_test.rb b/test/jobs/send_monthly_invoices_job_test.rb index 95937df73..9417278d7 100644 --- a/test/jobs/send_monthly_invoices_job_test.rb +++ b/test/jobs/send_monthly_invoices_job_test.rb @@ -94,6 +94,8 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase end end + perform_enqueued_jobs + invoice = Invoice.last assert_equal 309_902, invoice.number assert invoice.in_directo @@ -135,6 +137,8 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase end end + perform_enqueued_jobs + invoice = Invoice.last assert_equal 309_902, invoice.number assert invoice.in_directo @@ -171,6 +175,8 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase SendMonthlyInvoicesJob.perform_now end + perform_enqueued_jobs + invoice = Invoice.last assert_equal 309_902, invoice.number assert invoice.in_directo @@ -198,6 +204,8 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase SendMonthlyInvoicesJob.perform_now end + perform_enqueued_jobs + invoice = Invoice.last assert_equal 309_902, invoice.number assert invoice.in_directo @@ -244,6 +252,8 @@ class SendMonthlyInvoicesJobTest < ActiveSupport::TestCase end end + perform_enqueued_jobs + assert_requested first_registrar_stub assert_requested second_registrar_stub From 34f5347c1f57fa126b26ba2c48e9b33e17550212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Tue, 30 Aug 2022 14:15:45 +0300 Subject: [PATCH 07/10] Added product_id to monthly invoice items --- Gemfile.lock | 4 ++-- app/models/invoice/e_invoice_generator.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a6c822723..44290fffd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,10 +18,10 @@ GIT GIT remote: https://github.com/internetee/e_invoice.git - revision: da18f3da3219315f732b94fbc165fe83cb828a99 + revision: 590dcd3b769ea57edd4c4626547b37120a02b127 branch: master specs: - e_invoice (0.1.1) + e_invoice (0.1.2) builder (~> 3.2) nokogiri savon diff --git a/app/models/invoice/e_invoice_generator.rb b/app/models/invoice/e_invoice_generator.rb index 4830e486a..326984476 100644 --- a/app/models/invoice/e_invoice_generator.rb +++ b/app/models/invoice/e_invoice_generator.rb @@ -91,13 +91,13 @@ class Invoice def generate_monthly_invoice_item(invoice, item) EInvoice::InvoiceItem.new.tap do |i| i.description = item.description - i.description = "[#{item.product_id}] #{item.description}" if item.product_id + i.product_id = item.product_id i.unit = item.unit i.price = item.price i.quantity = item.quantity + i.vat_rate = invoice.vat_rate if item.price && item.quantity i.subtotal = (item.price * item.quantity).round(3) - i.vat_rate = invoice.vat_rate i.vat_amount = i.subtotal * (i.vat_rate / 100) i.total = i.subtotal + i.vat_amount end From 0a20f567c24457d50218fa6abdb6b6f45151c420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Tue, 30 Aug 2022 17:35:25 +0300 Subject: [PATCH 08/10] Added monthly invoice name attribute --- Gemfile.lock | 4 +-- app/models/invoice/e_invoice_generator.rb | 35 +++++++++-------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 44290fffd..fb51d2bf1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,10 +18,10 @@ GIT GIT remote: https://github.com/internetee/e_invoice.git - revision: 590dcd3b769ea57edd4c4626547b37120a02b127 + revision: 9f850465697a2448a31ebddb83c1be5a5a9be3d2 branch: master specs: - e_invoice (0.1.2) + e_invoice (0.1.3) builder (~> 3.2) nokogiri savon diff --git a/app/models/invoice/e_invoice_generator.rb b/app/models/invoice/e_invoice_generator.rb index 326984476..8d9675475 100644 --- a/app/models/invoice/e_invoice_generator.rb +++ b/app/models/invoice/e_invoice_generator.rb @@ -41,17 +41,16 @@ class Invoice e_invoice_invoice_items = [] invoice.each do |invoice_item| - if invoice.monthly_invoice - e_invoice_invoice_item = generate_monthly_invoice_item(invoice, invoice_item) - else - e_invoice_invoice_item = generate_normal_invoice_item(invoice_item) - 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 @@ -75,31 +74,23 @@ class Invoice private - def generate_normal_invoice_item(item) + 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 - i.subtotal = item.subtotal - i.vat_rate = item.vat_rate - i.vat_amount = item.vat_amount - i.total = item.total - end - end - - def generate_monthly_invoice_item(invoice, item) - EInvoice::InvoiceItem.new.tap do |i| - i.description = item.description - i.product_id = item.product_id - i.unit = item.unit - i.price = item.price - i.quantity = item.quantity - i.vat_rate = invoice.vat_rate - if item.price && 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 From 80c4057b8fcf2b2cc8fa315d7b681b5c0c22d0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Wed, 31 Aug 2022 14:22:01 +0300 Subject: [PATCH 09/10] Corrected monthly invoices for sending to omniva --- app/jobs/send_monthly_invoices_job.rb | 36 ++++++++++++++++++- app/models/concerns/registrar/book_keeping.rb | 7 ++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb index 647bd0752..00a003444 100644 --- a/app/jobs/send_monthly_invoices_job.rb +++ b/app/jobs/send_monthly_invoices_job.rb @@ -39,6 +39,7 @@ class SendMonthlyInvoicesJob < ApplicationJob sync_with_directo end end + # rubocop:enable Metrics/MethodLength def send_email_to_registrar(invoice:, registrar:) @@ -52,7 +53,7 @@ class SendMonthlyInvoicesJob < ApplicationJob end def create_invoice(summary, registrar) - invoice = registrar.init_monthly_invoice(summary) + invoice = registrar.init_monthly_invoice(normalize(summary)) invoice.number = assign_monthly_number return unless invoice.save! @@ -108,4 +109,37 @@ class SendMonthlyInvoicesJob < ApplicationJob Setting.directo_monthly_number_last = num.to_i end + + private + + 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 + + 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/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index 13574f3c2..c74ae3987 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -17,14 +17,11 @@ module Registrar::BookKeeping 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 @@ -92,7 +89,7 @@ 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:) From a03fb140a3ae98c2a55c4be54d657f87a68f4385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergei=20Ts=C3=B5ganov?= Date: Wed, 31 Aug 2022 14:27:43 +0300 Subject: [PATCH 10/10] Fixed codeclimate issues --- app/jobs/send_monthly_invoices_job.rb | 4 +++- app/models/concerns/registrar/book_keeping.rb | 2 +- app/models/registrar.rb | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/jobs/send_monthly_invoices_job.rb b/app/jobs/send_monthly_invoices_job.rb index 00a003444..0684dc992 100644 --- a/app/jobs/send_monthly_invoices_job.rb +++ b/app/jobs/send_monthly_invoices_job.rb @@ -1,4 +1,4 @@ -class SendMonthlyInvoicesJob < ApplicationJob +class SendMonthlyInvoicesJob < ApplicationJob # rubocop:disable Metrics/ClassLength queue_as :default def perform(dry: false) @@ -112,6 +112,7 @@ class SendMonthlyInvoicesJob < ApplicationJob private + # rubocop:disable Metrics/MethodLength def normalize(summary, lines: []) sum = summary.dup line_map = Hash.new 0 @@ -127,6 +128,7 @@ class SendMonthlyInvoicesJob < ApplicationJob sum['invoice_lines'] = summarize_lines(lines) sum end + # rubocop:enable Metrics/MethodLength def summarize_lines(invoice_lines, lines: []) line_map = Hash.new 0 diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index c74ae3987..fc1defe9a 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -1,4 +1,4 @@ -module Registrar::BookKeeping +module Registrar::BookKeeping # rubocop:disable Metrics/ModuleLength extend ActiveSupport::Concern DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI', diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 811fc3921..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