From faeb50cad5b7fc2dc92b1c307786b8e87bca1986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Wed, 5 Feb 2020 15:42:57 +0200 Subject: [PATCH 01/13] DirectoInvoiceForwardJob, send prepayments via Directo gem --- Gemfile | 2 + app/jobs/directo_invoice_forward_job_job.rb | 100 ++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 app/jobs/directo_invoice_forward_job_job.rb diff --git a/Gemfile b/Gemfile index b3882e792..f91077714 100644 --- a/Gemfile +++ b/Gemfile @@ -68,6 +68,8 @@ gem 'domain_name' gem 'haml', '~> 5.0' gem 'wkhtmltopdf-binary' +gem 'directo', github: 'internetee/directo', branch: 'directo-api' + group :development do # deploy gem 'mina', '0.3.1' # for fast deployment diff --git a/app/jobs/directo_invoice_forward_job_job.rb b/app/jobs/directo_invoice_forward_job_job.rb new file mode 100644 index 000000000..4c95bb366 --- /dev/null +++ b/app/jobs/directo_invoice_forward_job_job.rb @@ -0,0 +1,100 @@ +class DirectoInvoiceForwardJobJob < ApplicationJob + queue_as :default + + def perform(monthly: false, dry: false) + api_url = ENV['directo_invoice_url'] + sales_agent = Setting.directo_sales_agent + payment_term = Setting.directo_receipt_payment_term + @prepayment_product_id = Setting.directo_receipt_product_name + + @client = DirectoApi::Client.new(api_url, sales_agent, payment_term) + monthly ? send_monthly_invoices(dry: dry) : send_receipts(dry: dry) + end + + def send_receipts + unsent_invoices = Invoice.where(in_directo: false).non_cancelled + + Rails.logger.info("[DIRECTO] Trying to send #{unsent_invoices.count} prepayment invoices") + unsent_invoices.each do |invoice| + unless valid_invoice_conditions?(invoice) + Rails.logger.info("[DIRECTO] Invoice #{invoice.number} has been skipped") && next + end + + @client.invoices.add(generate_directo_invoice(invoice: invoice, client: @client, + product_id: @prepayment_product_id)) + end + sync_with_directo + end + + def send_monthly_invoices; end + + def valid_invoice_conditions?(invoice) + if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || + invoice.account_activity.bank_transaction.sum.nil? || + invoice.account_activity.bank_transaction.sum != invoice.total + false + end + true + end + + def generate_directo_invoice(invoice:, client:, product_id:) + inv = client.invoices.new + inv = create_invoice_meta(directo_invoice: inv, invoice: invoice) + inv = create_invoice_line(invoice: invoice, directo_invoice: inv, product_id: product_id) + + inv + end + + def create_invoice_meta(directo_invoice:, invoice:) + directo_invoice.customer = create_invoice_customer(invoice: invoice) + directo_invoice.date = invoice.issue_date.strftime('%Y-%m-%d') # Mapped + directo_invoice.transaction_date = + invoice.account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') # Mapped + directo_invoice.number = invoice.number # Mapped + directo_invoice.currency = invoice.currency # Mapped + directo_invoice.language = 'ENG' # Hardcoded + + directo_invoice + end + + def create_invoice_line(invoice:, directo_invoice:, product_id:) + line = directo_invoice.lines.new + line.code = product_id # MAPPED + line.description = invoice.result.auction.domain_name # MAPPED + line.quantity = 1 # MAPPED + line.price = ActionController::Base.helpers. + number_with_precision(invoice.subtotal, precision: 2, separator: ".") # MAPPED + directo_invoice.lines.add(line) + + directo_invoice + end + + def create_invoice_customer(invoice:) + customer = Directo::Customer.new + customer.code = invoice.buyer.accounting_customer_code # MAPPED + + customer + end + + def sync_with_directo + res = @client.invoices.deliver(ssl_verify: false) + Rails.logger.info("[Directo] Directo responded with code: #{res.code}, body: #{res.body}") + update_invoice_directo_state(res.body) if res.code == '200' + rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, + EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError + Rails.logger.info("[Directo] Failed. Responded with code: #{res.code}, body: #{res.body}") + end + + def update_invoice_directo_state(xml) + Nokogiri::XML(xml).css('Result').each do |res| + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + mark_invoice_as_sent(invoice: inv, data: res) + end + end + + def mark_invoice_as_sent(invoice:, data:) + invoice.directo_records.create!(response: data.as_json.to_h, invoice_number: invoice.number) + invoice.update_columns(in_directo: true) + Rails.logger.info("[DIRECTO] Invoice #{invoice.number} was pushed and return is #{data.as_json.to_h.inspect}") + end +end From d5662f42b8617d0dd5b25e1e06257045db71143a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Wed, 12 Feb 2020 17:14:18 +0200 Subject: [PATCH 02/13] Fully implemented prepayment Directo invoice forwarding, WIP on proformas --- Gemfile.lock | 12 +- app/controllers/concerns/book_keeping.rb | 99 +++++++++ app/jobs/directo_invoice_forward_job.rb | 101 +++++++++ app/jobs/directo_invoice_forward_job_job.rb | 100 --------- app/models/counter.rb | 24 --- app/models/directo.rb | 195 ------------------ app/models/invoice.rb | 18 +- app/models/registrar.rb | 1 + test/jobs/directo_invoice_forward_job_test.rb | 20 ++ test/models/directo_test.rb | 16 -- 10 files changed, 249 insertions(+), 337 deletions(-) create mode 100644 app/controllers/concerns/book_keeping.rb create mode 100644 app/jobs/directo_invoice_forward_job.rb delete mode 100644 app/jobs/directo_invoice_forward_job_job.rb delete mode 100644 app/models/counter.rb create mode 100644 test/jobs/directo_invoice_forward_job_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 23aa90a51..71abcf70d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,15 @@ GIT activesupport savon +GIT + remote: https://github.com/internetee/directo.git + revision: 6ac71939da589fcceb5ef3989ba982134679ec97 + branch: directo-api + specs: + directo (0.1.0) + money (~> 6.13) + nokogiri (~> 1.10) + GIT remote: https://github.com/internetee/e_invoice.git revision: 3a754974ed25569aa85d99a87ae9e131b7c10a24 @@ -457,6 +466,7 @@ DEPENDENCIES database_cleaner devise (~> 4.7) digidoc_client! + directo! domain_name e_invoice! epp! @@ -500,4 +510,4 @@ DEPENDENCIES wkhtmltopdf-binary BUNDLED WITH - 2.0.2 + 2.1.4 diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb new file mode 100644 index 000000000..4e25a466f --- /dev/null +++ b/app/controllers/concerns/book_keeping.rb @@ -0,0 +1,99 @@ +module BookKeeping + extend ActiveSupport::Concern + + DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI', + 'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze + + def monthly_summary(month:) + activities = monthly_activites(month) + inv = { + 'number': 1, + 'customer_code': accounting_customer_code, + 'language': language, + 'currency': activities.first.currency, + 'date': month.end_of_month.strftime('%Y-%m-%d'), + }.as_json + + lines = [] + activities.each do |activity| + fetch_invoice_lines(activity, lines) + end + lines << prepayment_for_all(lines) + + inv['invoice_lines'] = lines.as_json + + inv + end + + def fetch_invoice_lines(activity, lines) + price = load_price(activity) + if price.duration.include? 'year' + price.duration.to_i.times do |duration| + lines << new_montly_invoice_line(activity: activity, duration: duration + 1).as_json + end + else + lines << new_monthly_invoice_line(activity: activity).as_json + end + end + + def monthly_activites(month) + AccountActivity.where(account_id: account_ids) + .where(created_at: month.beginning_of_month..month.end_of_month) + .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) + end + + def new_montly_invoice_line(activity:, duration: nil) + price = DirectoInvoiceForwardJob.load_price(activity) + yearly = price.duration.include?('year') + line = { + 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], + 'quantity': 1, + 'price': yearly ? (price.price.amount / price.duration.to_i) : price.amount, + } + + line['description'] = description_in_language(price: price, yearly: yearly) + add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 + + line + end + + 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') + end + + def description_in_language(price:, yearly:) + if language == 'en' + registration_length = yearly ? 'year' : 'month' + prefix = ".#{price.zone_name} registration: #{price.duration.to_i} #{registration_length}" + suffix = 's' + else + registration_length = yearly ? 'aasta' : 'kuu' + prefix = ".#{price.zone_name} registreerimine: #{price.duration.to_i} #{registration_length}" + suffix = yearly ? 't' : 'd' + end + + return "#{prefix}#{suffix}" if price.duration.to_i > 1 + + prefix + end + + def prepayment_for_all(lines) + total = 0 + lines.each { |l| total += l['quantity'].to_f * l['price'].to_f } + { + 'product_id': Setting.directo_receipt_product_name, + 'description': 'Domeenide ettemaks', + 'quantity': -1, + 'price': total + } + end + + def load_price(account_activity) + @pricelists ||= {} + return @pricelists[account_activity.price_id] if @pricelists.key? account_activity.price_id + + @pricelists[account_activity.price_id] = account_activity.price + end +end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb new file mode 100644 index 000000000..6eca4c73c --- /dev/null +++ b/app/jobs/directo_invoice_forward_job.rb @@ -0,0 +1,101 @@ +class DirectoInvoiceForwardJob < Que::Job + def run(monthly: false, dry: false) + @dry = dry + api_url = ENV['directo_invoice_url'] + sales_agent = Setting.directo_sales_agent + payment_term = Setting.directo_receipt_payment_term + @prepayment_product_id = Setting.directo_receipt_product_name + + @client = DirectoApi::Client.new(api_url, sales_agent, payment_term) + monthly ? send_monthly_invoices : send_receipts + end + + def send_receipts + unsent_invoices = Invoice.where(in_directo: false).non_cancelled + + Rails.logger.info("[DIRECTO] Trying to send #{unsent_invoices.count} prepayment invoices") + unsent_invoices.each do |invoice| + unless valid_invoice_conditions?(invoice) + Rails.logger.info "[DIRECTO] Invoice #{invoice.number} has been skipped" + next + end + @client.invoices.add_with_schema(invoice: invoice.as_directo_json, schema: 'prepayment') + end + + sync_with_directo + end + + def send_monthly_invoices + month = Time.now - 1.month + + Registrar.where.not(test_registrar: true).find_each do |registrar| + next unless registrar.cash_account + + invoice = registrar.monthly_summary(month: month) + @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') + end + + # TODO: Invoice number + sync_with_directo + end + + def valid_invoice_conditions?(invoice) + if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || + invoice.account_activity.bank_transaction.sum.nil? || + invoice.account_activity.bank_transaction.sum != invoice.total + return false + + end + + true + end + + def sync_with_directo + Rails.logger.info('[Directo] - attempting to send following XML:') + puts @client.invoices.as_xml + + return if @dry + + res = @client.invoices.deliver(ssl_verify: false) + + update_invoice_directo_state(res.body) if res.code == '200' + rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, + EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError + Rails.logger.info("[Directo] Failed. Responded with code: #{res.code}, body: #{res.body}") + end + + def update_invoice_directo_state(xml) + Nokogiri::XML(xml).css('Result').each do |res| + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + mark_invoice_as_sent(invoice: inv, data: res) + end + end + + def mark_invoice_as_sent(invoice:, data:) + invoice.directo_records.create!(response: data.as_json.to_h, invoice_number: invoice.number) + invoice.update_columns(in_directo: true) + Rails.logger.info("[DIRECTO] Invoice #{invoice.number} was pushed and return is #{data.as_json.to_h.inspect}") + end + + def self.load_price(account_activity) + @pricelists ||= {} + if @pricelists.key? account_activity.price_id + return @pricelists[account_activity.price_id] + end + + @pricelists[account_activity.price_id] = account_activity.price + end + + def last_directo_monthly_number + min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) + max_directo = Setting.directo_monthly_number_max.presence.try(:to_i) + last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), min_directo] + .compact.max || 0 + + if max_directo && max_directo <= last_directo + raise 'Directo counter is out of period' + end + + last_directo + end +end diff --git a/app/jobs/directo_invoice_forward_job_job.rb b/app/jobs/directo_invoice_forward_job_job.rb deleted file mode 100644 index 4c95bb366..000000000 --- a/app/jobs/directo_invoice_forward_job_job.rb +++ /dev/null @@ -1,100 +0,0 @@ -class DirectoInvoiceForwardJobJob < ApplicationJob - queue_as :default - - def perform(monthly: false, dry: false) - api_url = ENV['directo_invoice_url'] - sales_agent = Setting.directo_sales_agent - payment_term = Setting.directo_receipt_payment_term - @prepayment_product_id = Setting.directo_receipt_product_name - - @client = DirectoApi::Client.new(api_url, sales_agent, payment_term) - monthly ? send_monthly_invoices(dry: dry) : send_receipts(dry: dry) - end - - def send_receipts - unsent_invoices = Invoice.where(in_directo: false).non_cancelled - - Rails.logger.info("[DIRECTO] Trying to send #{unsent_invoices.count} prepayment invoices") - unsent_invoices.each do |invoice| - unless valid_invoice_conditions?(invoice) - Rails.logger.info("[DIRECTO] Invoice #{invoice.number} has been skipped") && next - end - - @client.invoices.add(generate_directo_invoice(invoice: invoice, client: @client, - product_id: @prepayment_product_id)) - end - sync_with_directo - end - - def send_monthly_invoices; end - - def valid_invoice_conditions?(invoice) - if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || - invoice.account_activity.bank_transaction.sum.nil? || - invoice.account_activity.bank_transaction.sum != invoice.total - false - end - true - end - - def generate_directo_invoice(invoice:, client:, product_id:) - inv = client.invoices.new - inv = create_invoice_meta(directo_invoice: inv, invoice: invoice) - inv = create_invoice_line(invoice: invoice, directo_invoice: inv, product_id: product_id) - - inv - end - - def create_invoice_meta(directo_invoice:, invoice:) - directo_invoice.customer = create_invoice_customer(invoice: invoice) - directo_invoice.date = invoice.issue_date.strftime('%Y-%m-%d') # Mapped - directo_invoice.transaction_date = - invoice.account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') # Mapped - directo_invoice.number = invoice.number # Mapped - directo_invoice.currency = invoice.currency # Mapped - directo_invoice.language = 'ENG' # Hardcoded - - directo_invoice - end - - def create_invoice_line(invoice:, directo_invoice:, product_id:) - line = directo_invoice.lines.new - line.code = product_id # MAPPED - line.description = invoice.result.auction.domain_name # MAPPED - line.quantity = 1 # MAPPED - line.price = ActionController::Base.helpers. - number_with_precision(invoice.subtotal, precision: 2, separator: ".") # MAPPED - directo_invoice.lines.add(line) - - directo_invoice - end - - def create_invoice_customer(invoice:) - customer = Directo::Customer.new - customer.code = invoice.buyer.accounting_customer_code # MAPPED - - customer - end - - def sync_with_directo - res = @client.invoices.deliver(ssl_verify: false) - Rails.logger.info("[Directo] Directo responded with code: #{res.code}, body: #{res.body}") - update_invoice_directo_state(res.body) if res.code == '200' - rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, - EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError - Rails.logger.info("[Directo] Failed. Responded with code: #{res.code}, body: #{res.body}") - end - - def update_invoice_directo_state(xml) - Nokogiri::XML(xml).css('Result').each do |res| - inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) - mark_invoice_as_sent(invoice: inv, data: res) - end - end - - def mark_invoice_as_sent(invoice:, data:) - invoice.directo_records.create!(response: data.as_json.to_h, invoice_number: invoice.number) - invoice.update_columns(in_directo: true) - Rails.logger.info("[DIRECTO] Invoice #{invoice.number} was pushed and return is #{data.as_json.to_h.inspect}") - end -end diff --git a/app/models/counter.rb b/app/models/counter.rb deleted file mode 100644 index 7d1c2b926..000000000 --- a/app/models/counter.rb +++ /dev/null @@ -1,24 +0,0 @@ -class Counter - def initialize value = 0 - @value = value - end - attr_accessor :value - def method_missing *args, &blk - @value.send(*args, &blk) - end - def to_s - @value.to_s - end - - def now - @value - end - - # pre-increment ".+" when x not present - def next(x = 1) - @value += x - end - def prev(x = 1) - @value -= x - end -end \ No newline at end of file diff --git a/app/models/directo.rb b/app/models/directo.rb index 789db64b2..a4af6c134 100644 --- a/app/models/directo.rb +++ b/app/models/directo.rb @@ -1,198 +1,3 @@ class Directo < ApplicationRecord - DOMAIN_TO_PRODUCT = {"ee" => "01EE", "com.ee" => "02COM", "pri.ee" => "03PRI", "fie.ee"=>"04FIE", "med.ee" => "05MED"}.freeze belongs_to :item, polymorphic: true - - def self.send_receipts - new_trans = Invoice.where(in_directo: false).non_cancelled - total = new_trans.count - counter = 0 - Rails.logger.info("[DIRECTO] Will try to send #{total} invoices") - - new_trans.find_in_batches(batch_size: 10).each do |group| - mappers = {} # need them as no direct connection between invoice - builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| - xml.invoices { - group.each do |invoice| - - if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || - invoice.account_activity.bank_transaction.sum.nil? || invoice.account_activity.bank_transaction.sum != invoice.total - Rails.logger.info("[DIRECTO] Invoice #{invoice.number} has been skipped") - next - end - counter += 1 - - num = invoice.number - paid_at = invoice.account_activity.bank_transaction&.paid_at&.strftime("%Y-%m-%d") - mappers[num] = invoice - xml.invoice( - "SalesAgent" => Setting.directo_sales_agent, - "Number" => num, - "InvoiceDate" => invoice.issue_date.strftime("%Y-%m-%d"), - 'TransactionDate' => paid_at, - "PaymentTerm" => Setting.directo_receipt_payment_term, - "Currency" => invoice.currency, - "CustomerCode"=> invoice.buyer.accounting_customer_code - ){ - xml.line( - "ProductID" => Setting.directo_receipt_product_name, - "Quantity" => 1, - "UnitPriceWoVAT" => ActionController::Base.helpers.number_with_precision(invoice.subtotal, precision: 2, separator: "."), - "ProductName" => invoice.order - ) - } - end - } - end - - data = builder.to_xml.gsub("\n",'') - Rails.logger.info("[Directo] XML request: #{data}") - response = RestClient::Request.execute(url: ENV['directo_invoice_url'], method: :post, payload: {put: "1", what: "invoice", xmldata: data}, verify_ssl: false) - Rails.logger.info("[Directo] Directo responded with code: #{response.code}, body: #{response.body}") - dump_result_to_db(mappers, response.to_s) - end - - STDOUT << "#{Time.zone.now.utc} - Directo receipts sending finished. #{counter} of #{total} are sent\n" - end - - def self.dump_result_to_db mappers, xml - Nokogiri::XML(xml).css("Result").each do |res| - obj = mappers[res.attributes["docid"].value.to_i] - obj.directo_records.create!(response: res.as_json.to_h, invoice_number: obj.number) - obj.update_columns(in_directo: true) - Rails.logger.info("[DIRECTO] Invoice #{res.attributes["docid"].value} was pushed and return is #{res.as_json.to_h.inspect}") - end - end - - - def self.send_monthly_invoices(debug: false) - I18n.locale = :et - month = Time.now - 1.month - invoices_until = month.end_of_month - date_format = "%Y-%m-%d" - invoice_counter= Counter.new - - min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) - max_directo = Setting.directo_monthly_number_max.presence.try(:to_i) - last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), min_directo].compact.max || 0 - if max_directo && max_directo <= last_directo - raise "Directo counter is out of period (max allowed number is smaller than last counter number)" - end - - directo_next = last_directo - Registrar.where.not(test_registrar: true).find_each do |registrar| - unless registrar.cash_account - Rails.logger.info("[DIRECTO] Monthly invoice for registrar #{registrar.id} has been skipped as it doesn't has cash_account") - next - end - counter = Counter.new(1) - items = {} - registrar_activities = AccountActivity.where(account_id: registrar.account_ids).where("created_at BETWEEN ? AND ?",month.beginning_of_month, month.end_of_month) - - # adding domains items - registrar_activities.where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]).each do |activity| - price = load_price(activity) - - if price.duration.include?('year') - price.duration.to_i.times do |i| - year = i+1 - hash = { - "ProductID" => DOMAIN_TO_PRODUCT[price.zone_name], - "Unit" => "tk", - "ProductName" => ".#{price.zone_name} registreerimine: #{price.duration.to_i} aasta#{price.duration.to_i > 1 ? 't' : ''}", - "UnitPriceWoVAT" => price.price.amount / price.duration.to_i - } - hash["StartDate"] = (activity.created_at + (year-1).year).end_of_month.strftime(date_format) if year > 1 - hash["EndDate"] = (activity.created_at + (year-1).year + 1).end_of_month.strftime(date_format) if year > 1 - - if items.has_key?(hash) - items[hash]["Quantity"] += 1 - else - items[hash] = { "RN" => counter.next, "RR" => counter.now - i, "Quantity" => 1 } - end - end - else - 1.times do |i| - quantity = price.account_activities - .where(account_id: registrar.account_ids) - .where(created_at: month.beginning_of_month..month.end_of_month) - .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) - .count - - hash = { - "ProductID" => DOMAIN_TO_PRODUCT[price.zone_name], - "Unit" => "tk", - "ProductName" => ".#{price.zone_name} registreerimine: #{price.duration.to_i} kuud", - "UnitPriceWoVAT" => price.price.amount, - } - - if items.has_key?(hash) - #items[hash]["Quantity"] += 1 - else - items[hash] = { "RN" => counter.next, "RR" => counter.now - i, "Quantity" => quantity } - end - end - end - - - end - - #adding prepaiments - if items.any? - total = 0 - items.each{ |key, val| total += val["Quantity"] * key["UnitPriceWoVAT"] } - hash = {"ProductID" => Setting.directo_receipt_product_name, "Unit" => "tk", "ProductName" => "Domeenide ettemaks", "UnitPriceWoVAT"=>total} - items[hash] = {"RN"=>counter.next, "RR" => counter.now, "Quantity"=> -1} - end - - # generating XML - if items.any? - directo_next += 1 - invoice_counter.next - - builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| - xml.invoices{ - xml.invoice("Number" =>directo_next, - "InvoiceDate" =>invoices_until.strftime(date_format), - "PaymentTerm" =>Setting.directo_receipt_payment_term, - "CustomerCode"=>registrar.accounting_customer_code, - "Language" =>"", - "Currency" =>registrar_activities.first.currency, - "SalesAgent" =>Setting.directo_sales_agent){ - xml.line("RN" => 1, "RR"=>1, "ProductName"=> "Domeenide registreerimine - #{I18n.l(invoices_until, format: "%B %Y").titleize}") - items.each do |line, val| - xml.line(val.merge(line)) - end - } - } - end - - data = builder.to_xml.gsub("\n",'') - Rails.logger.info("[Directo] XML request: #{data}") - - if debug - STDOUT << "#{Time.zone.now.utc} - Directo xml had to be sent #{data}\n" - else - response = RestClient::Request.execute(url: ENV['directo_invoice_url'], method: :post, payload: {put: "1", what: "invoice", xmldata: data}, verify_ssl: false) - Rails.logger.info("[Directo] Directo responded with code: #{response.code}, body: #{response.body}") - response = response.to_s - - Setting.directo_monthly_number_last = directo_next - Nokogiri::XML(response).css("Result").each do |res| - Directo.create!(request: data, response: res.as_json.to_h, invoice_number: directo_next) - Rails.logger.info("[DIRECTO] Invoice #{res.attributes["docid"].value} was pushed and return is #{res.as_json.to_h.inspect}") - end - end - else - Rails.logger.info("[DIRECTO] Registrar #{registrar.id} has nothing to be sent to Directo") - end - - end - STDOUT << "#{Time.zone.now.utc} - Directo invoices sending finished. #{invoice_counter.now} are sent\n" - end - - def self.load_price(account_activity) - @pricelists ||= {} - return @pricelists[account_activity.price_id] if @pricelists.has_key?(account_activity.price_id) - @pricelists[account_activity.price_id] = account_activity.price - end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 7f1dea825..20d0c5091 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -102,6 +102,22 @@ class Invoice < ApplicationRecord generator.generate end + def as_directo_json + inv = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(self)) + inv['customer_code'] = buyer.accounting_customer_code + inv['issue_date'] = issue_date.strftime('%Y-%m-%d') + inv['transaction_date'] = account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') + inv['language'] = buyer.language + inv['invoice_lines'] = [{ + 'product_id': Setting.directo_receipt_product_name, + 'description': order, + 'quantity': 1, + 'price': ActionController::Base.helpers.number_with_precision(subtotal, precision: 2, separator: ".") + }].as_json + + inv + end + private def apply_default_buyer_vat_no @@ -111,4 +127,4 @@ class Invoice < ApplicationRecord def calculate_total self.total = subtotal + vat_amount end -end \ No newline at end of file +end diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 8f41d62ca..788d7857a 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -1,5 +1,6 @@ class Registrar < ApplicationRecord include Versions # version/registrar_version.rb + include BookKeeping has_many :domains, dependent: :restrict_with_error has_many :contacts, dependent: :restrict_with_error diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb new file mode 100644 index 000000000..47cf9e6fb --- /dev/null +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase + setup do + @invoice = invoices(:one) + end + + def test_xml_is_include_transaction_date + @invoice.update(total: @invoice.account_activity.bank_transaction.sum) + @invoice.account_activity.bank_transaction.update(paid_at: Time.zone.now) + + stub_request(:post, ENV['directo_invoice_url']).with do |request| + request.body.include? 'TransactionDate' + end + + assert_nothing_raised do + DirectoInvoiceForwardJob.run(monthly: false) + end + end +end diff --git a/test/models/directo_test.rb b/test/models/directo_test.rb index 9dbbf64d4..603a38d15 100644 --- a/test/models/directo_test.rb +++ b/test/models/directo_test.rb @@ -1,20 +1,4 @@ require 'test_helper' class DirectoTest < ActiveSupport::TestCase - setup do - @invoice = invoices(:one) - end - - def test_xml_is_include_transaction_date - @invoice.update(total: @invoice.account_activity.bank_transaction.sum) - @invoice.account_activity.bank_transaction.update(paid_at: Time.zone.now) - - stub_request(:post, ENV['directo_invoice_url']).with do |request| - request.body.include? 'TransactionDate' - end - - assert_nothing_raised do - Directo.send_receipts - end - end end From 98683f3bcc8111c8533d1c9e56d1c0a6dbeb488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Wed, 19 Feb 2020 11:53:15 +0200 Subject: [PATCH 03/13] Make sure that Directo monthly invoice number frame is not exceeded --- Gemfile.lock | 2 +- app/controllers/concerns/book_keeping.rb | 15 +++-- app/jobs/directo_invoice_forward_job.rb | 76 ++++++++++++++++-------- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 71abcf70d..e6b82297c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GIT GIT remote: https://github.com/internetee/directo.git - revision: 6ac71939da589fcceb5ef3989ba982134679ec97 + revision: 41f4b49da2d4155a76ab57f1cb07bb1d0ba9cdef branch: directo-api specs: directo (0.1.0) diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index 4e25a466f..284b6db71 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -11,7 +11,7 @@ module BookKeeping 'customer_code': accounting_customer_code, 'language': language, 'currency': activities.first.currency, - 'date': month.end_of_month.strftime('%Y-%m-%d'), + 'date': month.end_of_month.strftime('%Y-%m-%d') }.as_json lines = [] @@ -43,12 +43,13 @@ module BookKeeping end def new_montly_invoice_line(activity:, duration: nil) - price = DirectoInvoiceForwardJob.load_price(activity) + price = load_price(activity) yearly = price.duration.include?('year') line = { 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], 'quantity': 1, 'price': yearly ? (price.price.amount / price.duration.to_i) : price.amount, + 'unit': language == 'en' ? 'pc' : 'tk' } line['description'] = description_in_language(price: price, yearly: yearly) @@ -59,8 +60,9 @@ module 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') + start_date = (create_time + (duration - 1).year).end_of_month + end_date = (create_time + (duration - 1).year + 1).end_of_month + line['period'] = start_date..end_date end def description_in_language(price:, yearly:) @@ -84,9 +86,10 @@ module BookKeeping lines.each { |l| total += l['quantity'].to_f * l['price'].to_f } { 'product_id': Setting.directo_receipt_product_name, - 'description': 'Domeenide ettemaks', + 'description': language == 'en' ? 'Domains prepayment' : 'Domeenide ettemaks', 'quantity': -1, - 'price': total + 'price': total, + 'unit': language == 'en' ? 'pc' : 'tk' } end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 6eca4c73c..ff584914d 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -1,6 +1,7 @@ class DirectoInvoiceForwardJob < Que::Job def run(monthly: false, dry: false) @dry = dry + @monthly = monthly api_url = ENV['directo_invoice_url'] sales_agent = Setting.directo_sales_agent payment_term = Setting.directo_receipt_payment_term @@ -26,7 +27,7 @@ class DirectoInvoiceForwardJob < Que::Job end def send_monthly_invoices - month = Time.now - 1.month + month = Time.now Registrar.where.not(test_registrar: true).find_each do |registrar| next unless registrar.cash_account @@ -35,10 +36,25 @@ class DirectoInvoiceForwardJob < Que::Job @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') end - # TODO: Invoice number + assign_montly_numbers sync_with_directo end + def assign_montly_numbers + if directo_counter_exceedable?(@client.invoices.count) + raise 'Directo Counter is going to be out of period!' + end + + min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) + directo_number = [Setting.directo_monthly_number_last.presence.try(:to_i), + min_directo].compact.max || 0 + + @client.invoices.each do |inv| + directo_number += 1 + inv.number = directo_number + end + end + def valid_invoice_conditions?(invoice) if invoice.account_activity.nil? || invoice.account_activity.bank_transaction.nil? || invoice.account_activity.bank_transaction.sum.nil? || @@ -57,45 +73,53 @@ class DirectoInvoiceForwardJob < Que::Job return if @dry res = @client.invoices.deliver(ssl_verify: false) - - update_invoice_directo_state(res.body) if res.code == '200' + update_invoice_directo_state(res.body, @client.invoices.as_xml) if res.code == '200' rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError - Rails.logger.info("[Directo] Failed. Responded with code: #{res.code}, body: #{res.body}") + Rails.logger.info('[Directo] Failed to communicate via API') end - def update_invoice_directo_state(xml) + def update_invoice_directo_state(xml, req) + Rails.logger.info "[Directo] - Responded with body: #{xml}" Nokogiri::XML(xml).css('Result').each do |res| - inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) - mark_invoice_as_sent(invoice: inv, data: res) + if @monthly + mark_invoice_as_sent(res: res, req: req) + else + inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) + mark_invoice_as_sent(invoice: inv, res: res, req: req) + end end end - def mark_invoice_as_sent(invoice:, data:) - invoice.directo_records.create!(response: data.as_json.to_h, invoice_number: invoice.number) - invoice.update_columns(in_directo: true) - Rails.logger.info("[DIRECTO] Invoice #{invoice.number} was pushed and return is #{data.as_json.to_h.inspect}") - end - - def self.load_price(account_activity) - @pricelists ||= {} - if @pricelists.key? account_activity.price_id - return @pricelists[account_activity.price_id] + def mark_invoice_as_sent(invoice: nil, res:, req:) + directo_record = Directo.new(response: res.as_json.to_h, + request: req, invoice_number: res.attributes['docid'].value.to_i) + if invoice + directo_record.invoice = invoice + invoice.update_columns(in_directo: true) + else + update_directo_number(num: directo_record.invoice_number) end - @pricelists[account_activity.price_id] = account_activity.price + directo_record.save! end - def last_directo_monthly_number + def update_directo_number(num:) + return unless num.to_i > Setting.directo_monthly_number_last + + Setting.directo_monthly_number_last = num + end + + def directo_counter_exceedable?(invoice_count) min_directo = Setting.directo_monthly_number_min.presence.try(:to_i) max_directo = Setting.directo_monthly_number_max.presence.try(:to_i) - last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), min_directo] - .compact.max || 0 + last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), + min_directo].compact.max || 0 - if max_directo && max_directo <= last_directo - raise 'Directo counter is out of period' + if max_directo && max_directo < (last_directo + invoice_count) + true + else + false end - - last_directo end end From 26adaa974332fe34771c4ef1224cef1ccff5cbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 20 Feb 2020 15:05:20 +0200 Subject: [PATCH 04/13] Fix invoice linking with directo record --- app/jobs/directo_invoice_forward_job.rb | 5 ++--- test/jobs/directo_invoice_forward_job_test.rb | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index ff584914d..3f0b11486 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -67,8 +67,7 @@ class DirectoInvoiceForwardJob < Que::Job end def sync_with_directo - Rails.logger.info('[Directo] - attempting to send following XML:') - puts @client.invoices.as_xml + Rails.logger.info("[Directo] - attempting to send following XML:\n #{@client.invoices.as_xml}") return if @dry @@ -95,7 +94,7 @@ class DirectoInvoiceForwardJob < Que::Job directo_record = Directo.new(response: res.as_json.to_h, request: req, invoice_number: res.attributes['docid'].value.to_i) if invoice - directo_record.invoice = invoice + directo_record.item = invoice invoice.update_columns(in_directo: true) else update_directo_number(num: directo_record.invoice_number) diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb index b50fe378b..ede17ce9e 100644 --- a/test/jobs/directo_invoice_forward_job_test.rb +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -21,7 +21,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase end.to_return(status: 200, body: response) assert_nothing_raised do - Directo.send_receipts + DirectoInvoiceForwardJob.run(monthly: false, dry: false) end assert_not_empty @invoice.directo_records.first.request From ee332d8a7f080d9033aaecd63c5d67516c5c9213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Fri, 21 Feb 2020 14:20:04 +0200 Subject: [PATCH 05/13] Tests for DirectoInvoiceForwardJob --- Gemfile.lock | 2 +- app/controllers/concerns/book_keeping.rb | 31 +++-- app/jobs/directo_invoice_forward_job.rb | 6 +- test/fixtures/account_activities.yml | 2 +- test/jobs/directo_invoice_forward_job_test.rb | 114 ++++++++++++++++++ 5 files changed, 144 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4bc35aafc..4ca0aeb22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GIT GIT remote: https://github.com/internetee/directo.git - revision: 41f4b49da2d4155a76ab57f1cb07bb1d0ba9cdef + revision: c688c46134ce04f5a75b7a0563abc18cd9af030a branch: directo-api specs: directo (0.1.0) diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index 284b6db71..e9d3e9ee9 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -6,6 +6,8 @@ module BookKeeping def monthly_summary(month:) activities = monthly_activites(month) + return unless activities.any? + inv = { 'number': 1, 'customer_code': accounting_customer_code, @@ -15,6 +17,8 @@ module BookKeeping }.as_json lines = [] + + lines << { 'description': title_for_summary(month) } activities.each do |activity| fetch_invoice_lines(activity, lines) end @@ -25,11 +29,23 @@ module BookKeeping inv end + def title_for_summary(date) + if language == 'en' + I18n.with_locale('en') do + "Domains registrations - #{I18n.l(date, format: '%B %Y')}" + end + else + I18n.with_locale('et') do + "Domeenide registreerimine - #{I18n.l(date, format: '%B %Y')}" + end + end + end + def fetch_invoice_lines(activity, lines) price = load_price(activity) if price.duration.include? 'year' price.duration.to_i.times do |duration| - lines << new_montly_invoice_line(activity: activity, duration: duration + 1).as_json + lines << new_monthly_invoice_line(activity: activity, duration: duration + 1).as_json end else lines << new_monthly_invoice_line(activity: activity).as_json @@ -42,27 +58,28 @@ module BookKeeping .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) end - def new_montly_invoice_line(activity:, duration: nil) + def new_monthly_invoice_line(activity:, duration: nil) price = load_price(activity) yearly = price.duration.include?('year') line = { 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], 'quantity': 1, - 'price': yearly ? (price.price.amount / price.duration.to_i) : price.amount, + 'price': yearly ? (price.price.amount / price.duration.to_i) : price.price.amount, 'unit': language == 'en' ? 'pc' : 'tk' } line['description'] = description_in_language(price: price, yearly: yearly) - add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 + if yearly && duration + add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 + end line end def add_product_timeframe(line:, activity:, duration:) create_time = activity.created_at - start_date = (create_time + (duration - 1).year).end_of_month - end_date = (create_time + (duration - 1).year + 1).end_of_month - line['period'] = start_date..end_date + 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') end def description_in_language(price:, yearly:) diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 3f0b11486..cd228fdaf 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -33,6 +33,8 @@ class DirectoInvoiceForwardJob < Que::Job next unless registrar.cash_account invoice = registrar.monthly_summary(month: month) + next if invoice.nil? + @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') end @@ -104,9 +106,9 @@ class DirectoInvoiceForwardJob < Que::Job end def update_directo_number(num:) - return unless num.to_i > Setting.directo_monthly_number_last + return unless num.to_i > Setting.directo_monthly_number_last.to_i - Setting.directo_monthly_number_last = num + Setting.directo_monthly_number_last = num.to_i end def directo_counter_exceedable?(invoice_count) diff --git a/test/fixtures/account_activities.yml b/test/fixtures/account_activities.yml index dbe1dc2aa..8f883e424 100644 --- a/test/fixtures/account_activities.yml +++ b/test/fixtures/account_activities.yml @@ -2,4 +2,4 @@ one: account: cash invoice: one bank_transaction: one - created_at: <%= Time.zone.parse('2010-07-05 10:00') %> \ No newline at end of file + created_at: <%= Time.zone.parse('2010-07-05 10:00') %> diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb index ede17ce9e..bf92d77c0 100644 --- a/test/jobs/directo_invoice_forward_job_test.rb +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -3,6 +3,15 @@ require "test_helper" class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase setup do @invoice = invoices(:one) + @user = registrars(:bestnames) + travel_to Time.zone.parse('2010-07-06') + end + + def teardown + Setting.clear_cache + Setting.directo_monthly_number_min = 309901 + Setting.directo_monthly_number_max = 309999 + Setting.directo_monthly_number_last = 309901 end def test_xml_is_include_transaction_date @@ -26,4 +35,109 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase assert_not_empty @invoice.directo_records.first.request end + + def test_fails_if_directo_bounds_exceedable + Setting.clear_cache + Setting.directo_monthly_number_max = 30990 + + assert_raises 'RuntimeError' do + DirectoInvoiceForwardJob.run(monthly: true, dry: false) + end + 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') && + (body.include? 'Domeenide ettemaks') && + (body.include? '309902') + end.to_return(status: 200, body: response) + + assert_difference 'Setting.directo_monthly_number_last' do + DirectoInvoiceForwardJob.run(monthly: true, dry: false) + end + end + + def test_monthly_summary_is_delivered_in_english + activity = account_activities(:one) + price = billing_prices(:create_one_year) + activity.update(activity_type: 'create', price: price) + @user.update(language: 'en') + + response = <<-XML + + + + + XML + + stub_request(:post, ENV['directo_invoice_url']).with do |request| + body = CGI.unescape(request.body) + (body.include? 'test registration') && + (body.include? 'Domains prepayment') && + (body.include? '309902') + end.to_return(status: 200, body: response) + + assert_difference 'Setting.directo_monthly_number_last' do + DirectoInvoiceForwardJob.run(monthly: true, dry: false) + end + 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 + DirectoInvoiceForwardJob.run(monthly: true, dry: false) + end + 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? 'months') + end.to_return(status: 200, body: response) + + assert_difference 'Setting.directo_monthly_number_last' do + DirectoInvoiceForwardJob.run(monthly: true, dry: false) + end + end end From 5816ae16637c27d7dceaa216410e9a138df70187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 27 Feb 2020 11:32:29 +0200 Subject: [PATCH 06/13] Fix Rubocop styling issues --- app/controllers/concerns/book_keeping.rb | 6 +++--- app/jobs/directo_invoice_forward_job.rb | 2 +- app/models/invoice.rb | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index e9d3e9ee9..486a3c82c 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -13,7 +13,7 @@ module BookKeeping 'customer_code': accounting_customer_code, 'language': language, 'currency': activities.first.currency, - 'date': month.end_of_month.strftime('%Y-%m-%d') + 'date': month.end_of_month.strftime('%Y-%m-%d'), }.as_json lines = [] @@ -65,7 +65,7 @@ module BookKeeping 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], 'quantity': 1, 'price': yearly ? (price.price.amount / price.duration.to_i) : price.price.amount, - 'unit': language == 'en' ? 'pc' : 'tk' + 'unit': language == 'en' ? 'pc' : 'tk', } line['description'] = description_in_language(price: price, yearly: yearly) @@ -106,7 +106,7 @@ module BookKeeping 'description': language == 'en' ? 'Domains prepayment' : 'Domeenide ettemaks', 'quantity': -1, 'price': total, - 'unit': language == 'en' ? 'pc' : 'tk' + 'unit': language == 'en' ? 'pc' : 'tk', } end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index cd228fdaf..4daa65058 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -27,7 +27,7 @@ class DirectoInvoiceForwardJob < Que::Job end def send_monthly_invoices - month = Time.now + month = Time.zone.now - 1.month Registrar.where.not(test_registrar: true).find_each do |registrar| next unless registrar.cash_account diff --git a/app/models/invoice.rb b/app/models/invoice.rb index a1fb4cdb7..70053b59c 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -112,7 +112,8 @@ class Invoice < ApplicationRecord 'product_id': Setting.directo_receipt_product_name, 'description': order, 'quantity': 1, - 'price': ActionController::Base.helpers.number_with_precision(subtotal, precision: 2, separator: ".") + 'price': ActionController::Base.helpers + .number_with_precision(subtotal, precision: 2, separator: '.'), }].as_json inv From 756a8b0c1cb9569b105846334739ef1b46005af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 27 Feb 2020 14:24:22 +0200 Subject: [PATCH 07/13] Travel 1 month forward to get last month summaries --- test/jobs/directo_invoice_forward_job_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb index bf92d77c0..fd483d621 100644 --- a/test/jobs/directo_invoice_forward_job_test.rb +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -4,7 +4,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase setup do @invoice = invoices(:one) @user = registrars(:bestnames) - travel_to Time.zone.parse('2010-07-06') + travel_to Time.zone.parse('2010-08-06') end def teardown From 6302462184ecc204d290cd09f8969bf878955220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 27 Feb 2020 16:10:07 +0200 Subject: [PATCH 08/13] Directo job styling fixes --- app/controllers/concerns/book_keeping.rb | 33 +++++++++---------- app/jobs/directo_invoice_forward_job.rb | 16 ++++----- test/jobs/directo_invoice_forward_job_test.rb | 4 +-- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index 486a3c82c..24d6f5725 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -11,11 +11,16 @@ module BookKeeping inv = { 'number': 1, 'customer_code': accounting_customer_code, - 'language': language, - 'currency': activities.first.currency, + 'language': language, 'currency': activities.first.currency, 'date': month.end_of_month.strftime('%Y-%m-%d'), }.as_json + inv['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) + + inv + end + + def prepare_invoice_lines(month:, activities:) lines = [] lines << { 'description': title_for_summary(month) } @@ -24,9 +29,7 @@ module BookKeeping end lines << prepayment_for_all(lines) - inv['invoice_lines'] = lines.as_json - - inv + lines.as_json end def title_for_summary(date) @@ -83,19 +86,15 @@ module BookKeeping end def description_in_language(price:, yearly:) - if language == 'en' - registration_length = yearly ? 'year' : 'month' - prefix = ".#{price.zone_name} registration: #{price.duration.to_i} #{registration_length}" - suffix = 's' - else - registration_length = yearly ? 'aasta' : 'kuu' - prefix = ".#{price.zone_name} registreerimine: #{price.duration.to_i} #{registration_length}" - suffix = yearly ? 't' : 'd' - end + en = language == 'en' + registration_length = if yearly + en ? 'year(s)' : 'aasta(t)' + else + en ? 'month(s)' : 'kuu(d)' + end - return "#{prefix}#{suffix}" if price.duration.to_i > 1 - - prefix + registration = en ? 'registration' : 'registreerimine' + ".#{price.zone_name} #{registration}: #{price.duration.to_i} #{registration_length}" end def prepayment_for_all(lines) diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 4daa65058..3dc92c460 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -1,7 +1,7 @@ class DirectoInvoiceForwardJob < Que::Job def run(monthly: false, dry: false) @dry = dry - @monthly = monthly + (@month = Time.zone.now - 1.month) if monthly api_url = ENV['directo_invoice_url'] sales_agent = Setting.directo_sales_agent payment_term = Setting.directo_receipt_payment_term @@ -27,15 +27,11 @@ class DirectoInvoiceForwardJob < Que::Job end def send_monthly_invoices - month = Time.zone.now - 1.month - - Registrar.where.not(test_registrar: true).find_each do |registrar| + Registrar.where(test_registrar: false).find_each do |registrar| next unless registrar.cash_account - invoice = registrar.monthly_summary(month: month) - next if invoice.nil? - - @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') + invoice = registrar.monthly_summary(month: @month) + @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') unless invoice.nil? end assign_montly_numbers @@ -83,7 +79,7 @@ class DirectoInvoiceForwardJob < Que::Job def update_invoice_directo_state(xml, req) Rails.logger.info "[Directo] - Responded with body: #{xml}" Nokogiri::XML(xml).css('Result').each do |res| - if @monthly + if @month mark_invoice_as_sent(res: res, req: req) else inv = Invoice.find_by(number: res.attributes['docid'].value.to_i) @@ -97,7 +93,7 @@ class DirectoInvoiceForwardJob < Que::Job request: req, invoice_number: res.attributes['docid'].value.to_i) if invoice directo_record.item = invoice - invoice.update_columns(in_directo: true) + invoice.update(in_directo: true) else update_directo_number(num: directo_record.invoice_number) end diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb index fd483d621..378dd7aec 100644 --- a/test/jobs/directo_invoice_forward_job_test.rb +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -61,7 +61,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase stub_request(:post, ENV['directo_invoice_url']).with do |request| body = CGI.unescape(request.body) - (body.include? '.test registreerimine: 1 aasta') && + (body.include? '.test registreerimine: 1 aasta(t)') && (body.include? 'Domeenide ettemaks') && (body.include? '309902') end.to_return(status: 200, body: response) @@ -133,7 +133,7 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase stub_request(:post, ENV['directo_invoice_url']).with do |request| body = CGI.unescape(request.body) - (body.include? 'months') + body.include? 'month(s)' end.to_return(status: 200, body: response) assert_difference 'Setting.directo_monthly_number_last' do From 6c328438e69a6c48a6611938c12b948904d98812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 27 Feb 2020 17:06:25 +0200 Subject: [PATCH 09/13] Reduce condition sizes for Directo job --- app/controllers/concerns/book_keeping.rb | 14 +++++++++---- app/jobs/directo_invoice_forward_job.rb | 21 ++++++++++++------- app/models/invoice.rb | 17 ++++++++------- test/jobs/directo_invoice_forward_job_test.rb | 7 +++++-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index 24d6f5725..41cd4646e 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -12,7 +12,7 @@ module BookKeeping 'number': 1, 'customer_code': accounting_customer_code, 'language': language, 'currency': activities.first.currency, - 'date': month.end_of_month.strftime('%Y-%m-%d'), + 'date': month.end_of_month.strftime('%Y-%m-%d') }.as_json inv['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) @@ -63,16 +63,22 @@ module BookKeeping def new_monthly_invoice_line(activity:, duration: nil) price = load_price(activity) - yearly = price.duration.include?('year') line = { 'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym], 'quantity': 1, - 'price': yearly ? (price.price.amount / price.duration.to_i) : price.price.amount, 'unit': language == 'en' ? 'pc' : 'tk', } + finalize_invoice_line(line, price: price, duration: duration, activity: activity) + end + + def finalize_invoice_line(line, price:, activity:, duration:) + yearly = price.duration.include?('year') + + line['price'] = yearly ? (price.price.amount / price.duration.to_i) : price.price.amount line['description'] = description_in_language(price: price, yearly: yearly) - if yearly && duration + + if duration.present? add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 3dc92c460..768c7db0b 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -27,18 +27,23 @@ class DirectoInvoiceForwardJob < Que::Job end def send_monthly_invoices - Registrar.where(test_registrar: false).find_each do |registrar| - next unless registrar.cash_account - - invoice = registrar.monthly_summary(month: @month) - @client.invoices.add_with_schema(invoice: invoice, schema: 'summary') unless invoice.nil? + Registrar.where.not(test_registrar: true).find_each do |registrar| + fetch_monthly_summary(registrar: registrar) end - assign_montly_numbers + return unless @client.invoices.count.positive? + sync_with_directo end - def assign_montly_numbers + def fetch_monthly_summary(registrar:) + return unless registrar.cash_account + + summary = registrar.monthly_summary(month: @month) + @client.invoices.add_with_schema(invoice: summary, schema: 'summary') unless summary.nil? + end + + def assign_monthly_numbers if directo_counter_exceedable?(@client.invoices.count) raise 'Directo Counter is going to be out of period!' end @@ -66,9 +71,9 @@ class DirectoInvoiceForwardJob < Que::Job def sync_with_directo Rails.logger.info("[Directo] - attempting to send following XML:\n #{@client.invoices.as_xml}") - return if @dry + assign_monthly_numbers if @month res = @client.invoices.deliver(ssl_verify: false) update_invoice_directo_state(res.body, @client.invoices.as_xml) if res.code == '200' rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 70053b59c..61b35ab98 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -70,7 +70,7 @@ class Invoice < ApplicationRecord Country.new(buyer_country_code) end -# order is used for directo/banklink description + # order is used for directo/banklink description def order "Order nr. #{number}" end @@ -108,17 +108,18 @@ class Invoice < ApplicationRecord inv['issue_date'] = issue_date.strftime('%Y-%m-%d') inv['transaction_date'] = account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') inv['language'] = buyer.language - inv['invoice_lines'] = [{ - 'product_id': Setting.directo_receipt_product_name, - 'description': order, - 'quantity': 1, - 'price': ActionController::Base.helpers - .number_with_precision(subtotal, precision: 2, separator: '.'), - }].as_json + inv['invoice_lines'] = compose_directo_product inv end + def compose_directo_product + [{ 'product_id': Setting.directo_receipt_product_name, 'description': order, + 'quantity': 1, 'price': ActionController::Base.helpers.number_with_precision( + subtotal, precision: 2, separator: '.' + ) }].as_json + end + def do_not_send_e_invoice? e_invoice_sent? || cancelled? || paid? end diff --git a/test/jobs/directo_invoice_forward_job_test.rb b/test/jobs/directo_invoice_forward_job_test.rb index 378dd7aec..8a4fb43aa 100644 --- a/test/jobs/directo_invoice_forward_job_test.rb +++ b/test/jobs/directo_invoice_forward_job_test.rb @@ -37,8 +37,11 @@ class DirectoInvoiceForwardJobTest < ActiveSupport::TestCase end def test_fails_if_directo_bounds_exceedable - Setting.clear_cache - Setting.directo_monthly_number_max = 30990 + activity = account_activities(:one) + price = billing_prices(:create_one_year) + activity.update!(activity_type: 'create', price: price) + + Setting.directo_monthly_number_max = 30991 assert_raises 'RuntimeError' do DirectoInvoiceForwardJob.run(monthly: true, dry: false) From 035bb14d9f1ade829273d65b98fd97e036eb3aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Mon, 2 Mar 2020 12:23:15 +0200 Subject: [PATCH 10/13] Move directo translations from logic to I18n --- app/controllers/concerns/book_keeping.rb | 13 +++++-------- app/jobs/directo_invoice_forward_job.rb | 8 +++----- config/locales/registrars.en.yml | 5 ++++- config/locales/registrars.et.yml | 4 ++++ 4 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 config/locales/registrars.et.yml diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb index 41cd4646e..f50393822 100644 --- a/app/controllers/concerns/book_keeping.rb +++ b/app/controllers/concerns/book_keeping.rb @@ -92,15 +92,12 @@ module BookKeeping end def description_in_language(price:, yearly:) - en = language == 'en' - registration_length = if yearly - en ? 'year(s)' : 'aasta(t)' - else - en ? 'month(s)' : 'kuu(d)' - end + timeframe_string = yearly ? 'yearly' : 'monthly' + locale_string = ".registrars.invoice_#{timeframe_string}_product_description" - registration = en ? 'registration' : 'registreerimine' - ".#{price.zone_name} #{registration}: #{price.duration.to_i} #{registration_length}" + I18n.with_locale(language == 'en' ? 'en' : 'et') do + I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.to_i) + end end def prepayment_for_all(lines) diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index 768c7db0b..a423ffc2e 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -118,10 +118,8 @@ class DirectoInvoiceForwardJob < Que::Job last_directo = [Setting.directo_monthly_number_last.presence.try(:to_i), min_directo].compact.max || 0 - if max_directo && max_directo < (last_directo + invoice_count) - true - else - false - end + return true if max_directo && max_directo < (last_directo + invoice_count) + + false end end diff --git a/config/locales/registrars.en.yml b/config/locales/registrars.en.yml index 609f9f94a..c5071c3f8 100644 --- a/config/locales/registrars.en.yml +++ b/config/locales/registrars.en.yml @@ -1,4 +1,7 @@ en: + registrars: + invoice_yearly_product_description: '%{tld} registration: %{length} year(s)' + invoice_monthly_product_description: '%{tld} registration: %{length} month(s)' activerecord: errors: models: @@ -8,4 +11,4 @@ en: forbidden: is forbidden vat_rate: present: >- - must be blank when a registrar is VAT-registered in the same country as registry \ No newline at end of file + must be blank when a registrar is VAT-registered in the same country as registry diff --git a/config/locales/registrars.et.yml b/config/locales/registrars.et.yml new file mode 100644 index 000000000..4151183ea --- /dev/null +++ b/config/locales/registrars.et.yml @@ -0,0 +1,4 @@ +et: + registrars: + invoice_yearly_product_description: '%{tld} registration: %{length} year(s)' + invoice_monthly_product_description: '%{tld} registration: %{length} month(s)' From 3f92640ad2f24a92c8bf746afb4cf52e90e11e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Mon, 2 Mar 2020 13:54:46 +0200 Subject: [PATCH 11/13] Move registrar directo logic to concern, touch up translations --- app/controllers/concerns/book_keeping.rb | 121 ------------------ app/jobs/directo_invoice_forward_job.rb | 6 +- app/models/concerns/invoice/book_keeping.rb | 25 ++++ app/models/concerns/registrar/book_keeping.rb | 120 +++++++++++++++++ app/models/invoice.rb | 19 +-- app/models/registrar.rb | 2 +- config/locales/registrars.en.yml | 3 +- config/locales/registrars.et.yml | 7 +- 8 files changed, 156 insertions(+), 147 deletions(-) delete mode 100644 app/controllers/concerns/book_keeping.rb create mode 100644 app/models/concerns/invoice/book_keeping.rb create mode 100644 app/models/concerns/registrar/book_keeping.rb diff --git a/app/controllers/concerns/book_keeping.rb b/app/controllers/concerns/book_keeping.rb deleted file mode 100644 index f50393822..000000000 --- a/app/controllers/concerns/book_keeping.rb +++ /dev/null @@ -1,121 +0,0 @@ -module BookKeeping - extend ActiveSupport::Concern - - DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI', - 'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze - - def monthly_summary(month:) - activities = monthly_activites(month) - return unless activities.any? - - inv = { - 'number': 1, - 'customer_code': accounting_customer_code, - 'language': language, 'currency': activities.first.currency, - 'date': month.end_of_month.strftime('%Y-%m-%d') - }.as_json - - inv['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) - - inv - end - - def prepare_invoice_lines(month:, activities:) - lines = [] - - lines << { 'description': title_for_summary(month) } - activities.each do |activity| - fetch_invoice_lines(activity, lines) - end - lines << prepayment_for_all(lines) - - lines.as_json - end - - def title_for_summary(date) - if language == 'en' - I18n.with_locale('en') do - "Domains registrations - #{I18n.l(date, format: '%B %Y')}" - end - else - I18n.with_locale('et') do - "Domeenide registreerimine - #{I18n.l(date, format: '%B %Y')}" - end - end - end - - def fetch_invoice_lines(activity, lines) - price = load_price(activity) - if price.duration.include? 'year' - price.duration.to_i.times do |duration| - lines << new_monthly_invoice_line(activity: activity, duration: duration + 1).as_json - end - else - lines << new_monthly_invoice_line(activity: activity).as_json - end - end - - def monthly_activites(month) - AccountActivity.where(account_id: account_ids) - .where(created_at: month.beginning_of_month..month.end_of_month) - .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) - 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', - } - - finalize_invoice_line(line, price: price, duration: duration, activity: activity) - end - - def finalize_invoice_line(line, price:, activity:, duration:) - yearly = price.duration.include?('year') - - line['price'] = yearly ? (price.price.amount / price.duration.to_i) : price.price.amount - line['description'] = description_in_language(price: price, yearly: yearly) - - if duration.present? - add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 - end - - line - end - - 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') - end - - def description_in_language(price:, yearly:) - timeframe_string = yearly ? 'yearly' : 'monthly' - locale_string = ".registrars.invoice_#{timeframe_string}_product_description" - - I18n.with_locale(language == 'en' ? 'en' : 'et') do - I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.to_i) - end - end - - def prepayment_for_all(lines) - total = 0 - lines.each { |l| total += l['quantity'].to_f * l['price'].to_f } - { - 'product_id': Setting.directo_receipt_product_name, - 'description': language == 'en' ? 'Domains prepayment' : 'Domeenide ettemaks', - 'quantity': -1, - 'price': total, - 'unit': language == 'en' ? 'pc' : 'tk', - } - end - - def load_price(account_activity) - @pricelists ||= {} - return @pricelists[account_activity.price_id] if @pricelists.key? account_activity.price_id - - @pricelists[account_activity.price_id] = account_activity.price - end -end diff --git a/app/jobs/directo_invoice_forward_job.rb b/app/jobs/directo_invoice_forward_job.rb index a423ffc2e..6c3eb034c 100644 --- a/app/jobs/directo_invoice_forward_job.rb +++ b/app/jobs/directo_invoice_forward_job.rb @@ -70,18 +70,18 @@ class DirectoInvoiceForwardJob < Que::Job end def sync_with_directo + assign_monthly_numbers if @month Rails.logger.info("[Directo] - attempting to send following XML:\n #{@client.invoices.as_xml}") return if @dry - assign_monthly_numbers if @month res = @client.invoices.deliver(ssl_verify: false) - update_invoice_directo_state(res.body, @client.invoices.as_xml) if res.code == '200' + process_directo_response(res.body, @client.invoices.as_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 update_invoice_directo_state(xml, req) + def process_directo_response(xml, req) Rails.logger.info "[Directo] - Responded with body: #{xml}" Nokogiri::XML(xml).css('Result').each do |res| if @month diff --git a/app/models/concerns/invoice/book_keeping.rb b/app/models/concerns/invoice/book_keeping.rb new file mode 100644 index 000000000..828ad7848 --- /dev/null +++ b/app/models/concerns/invoice/book_keeping.rb @@ -0,0 +1,25 @@ +module Concerns + module Invoice + module BookKeeping + extend ActiveSupport::Concern + + def as_directo_json + inv = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(self)) + inv['customer_code'] = buyer.accounting_customer_code + inv['issue_date'] = issue_date.strftime('%Y-%m-%d') + inv['transaction_date'] = account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') + inv['language'] = buyer.language == 'en' ? 'ENG' : '' + inv['invoice_lines'] = compose_directo_product + + inv + end + + def compose_directo_product + [{ 'product_id': Setting.directo_receipt_product_name, 'description': order, + 'quantity': 1, 'price': ActionController::Base.helpers.number_with_precision( + subtotal, precision: 2, separator: '.' + ) }].as_json + end + end + end +end diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb new file mode 100644 index 000000000..c85aac317 --- /dev/null +++ b/app/models/concerns/registrar/book_keeping.rb @@ -0,0 +1,120 @@ +module Concerns + module Registrar + module BookKeeping + extend ActiveSupport::Concern + + DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI', + 'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze + + def monthly_summary(month:) + activities = monthly_activites(month) + return unless activities.any? + + inv = { + 'number': 1, + 'customer_code': accounting_customer_code, + 'language': language == 'en' ? 'ENG' : '', 'currency': activities.first.currency, + 'date': month.end_of_month.strftime('%Y-%m-%d') + }.as_json + + inv['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) + + inv + end + + def prepare_invoice_lines(month:, activities:) + lines = [] + + lines << { 'description': title_for_summary(month) } + activities.each do |activity| + fetch_invoice_lines(activity, lines) + end + lines << prepayment_for_all(lines) + + lines.as_json + end + + def title_for_summary(date) + I18n.with_locale(language == 'en' ? 'en' : 'et') do + I18n.t('registrar.monthly_summary_title', date: I18n.l(date, format: '%B %Y')) + end + end + + def fetch_invoice_lines(activity, lines) + price = load_price(activity) + if price.duration.include? 'year' + price.duration.to_i.times do |duration| + lines << new_monthly_invoice_line(activity: activity, duration: duration + 1).as_json + end + else + lines << new_monthly_invoice_line(activity: activity).as_json + end + end + + def monthly_activites(month) + AccountActivity.where(account_id: account_ids) + .where(created_at: month.beginning_of_month..month.end_of_month) + .where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW]) + 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', + } + + finalize_invoice_line(line, price: price, duration: duration, activity: activity) + end + + def finalize_invoice_line(line, price:, activity:, duration:) + yearly = price.duration.include?('year') + + line['price'] = yearly ? (price.price.amount / price.duration.to_i) : price.price.amount + line['description'] = description_in_language(price: price, yearly: yearly) + + if duration.present? + add_product_timeframe(line: line, activity: activity, duration: duration) if duration > 1 + end + + line + end + + 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') + end + + def description_in_language(price:, yearly:) + timeframe_string = yearly ? 'yearly' : 'monthly' + locale_string = "registrar.invoice_#{timeframe_string}_product_description" + + I18n.with_locale(language == 'en' ? 'en' : 'et') do + I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.to_i) + end + end + + def prepayment_for_all(lines) + total = 0 + en = language == 'en' + lines.each { |l| total += l['quantity'].to_f * l['price'].to_f } + { + 'product_id': Setting.directo_receipt_product_name, + 'description': en ? 'Domains prepayment' : 'Domeenide ettemaks', + 'quantity': -1, + 'price': total, + 'unit': en ? 'pc' : 'tk', + } + end + + def load_price(account_activity) + @pricelists ||= {} + return @pricelists[account_activity.price_id] if @pricelists.key? account_activity.price_id + + @pricelists[account_activity.price_id] = account_activity.price + end + end + end +end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 61b35ab98..7e1fee91b 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -2,6 +2,7 @@ class Invoice < ApplicationRecord include Versions include Concerns::Invoice::Cancellable include Concerns::Invoice::Payable + include Concerns::Invoice::BookKeeping belongs_to :buyer, class_name: 'Registrar' has_one :account_activity @@ -102,24 +103,6 @@ class Invoice < ApplicationRecord generator.generate end - def as_directo_json - inv = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(self)) - inv['customer_code'] = buyer.accounting_customer_code - inv['issue_date'] = issue_date.strftime('%Y-%m-%d') - inv['transaction_date'] = account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') - inv['language'] = buyer.language - inv['invoice_lines'] = compose_directo_product - - inv - end - - def compose_directo_product - [{ 'product_id': Setting.directo_receipt_product_name, 'description': order, - 'quantity': 1, 'price': ActionController::Base.helpers.number_with_precision( - subtotal, precision: 2, separator: '.' - ) }].as_json - end - def do_not_send_e_invoice? e_invoice_sent? || cancelled? || paid? end diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 8aae1e89e..c3522859e 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -1,6 +1,6 @@ class Registrar < ApplicationRecord include Versions # version/registrar_version.rb - include BookKeeping + include Concerns::Registrar::BookKeeping has_many :domains, dependent: :restrict_with_error has_many :contacts, dependent: :restrict_with_error diff --git a/config/locales/registrars.en.yml b/config/locales/registrars.en.yml index c5071c3f8..c57f2e891 100644 --- a/config/locales/registrars.en.yml +++ b/config/locales/registrars.en.yml @@ -1,7 +1,8 @@ en: - registrars: + registrar: invoice_yearly_product_description: '%{tld} registration: %{length} year(s)' invoice_monthly_product_description: '%{tld} registration: %{length} month(s)' + monthly_summary_title: 'Domain registrations - %{date}' activerecord: errors: models: diff --git a/config/locales/registrars.et.yml b/config/locales/registrars.et.yml index 4151183ea..1001638c1 100644 --- a/config/locales/registrars.et.yml +++ b/config/locales/registrars.et.yml @@ -1,4 +1,5 @@ et: - registrars: - invoice_yearly_product_description: '%{tld} registration: %{length} year(s)' - invoice_monthly_product_description: '%{tld} registration: %{length} month(s)' + registrar: + invoice_yearly_product_description: '%{tld} registreerimine: %{length} aasta(t)' + invoice_monthly_product_description: '%{tld} registreerimine: %{length} kuu(d)' + monthly_summary_title: 'Domeenide registreerimine - %{date}' From 21543d8416ad0586deade9ee163e1382e80703fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Tue, 3 Mar 2020 12:07:05 +0200 Subject: [PATCH 12/13] Better naming for variables --- Gemfile.lock | 2 +- app/models/concerns/invoice/book_keeping.rb | 15 ++++++++------- app/models/concerns/registrar/book_keeping.rb | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8875293cf..3b36f1935 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GIT GIT remote: https://github.com/internetee/directo.git - revision: 6fb158c1589c609b2519d8e8658c11de52bd3d9d + revision: 7bac1d5ca413e3cce25a7adad3c0e8d7f8cd673e branch: directo-api specs: directo (0.1.0) diff --git a/app/models/concerns/invoice/book_keeping.rb b/app/models/concerns/invoice/book_keeping.rb index 828ad7848..2469f45eb 100644 --- a/app/models/concerns/invoice/book_keeping.rb +++ b/app/models/concerns/invoice/book_keeping.rb @@ -4,14 +4,15 @@ module Concerns extend ActiveSupport::Concern def as_directo_json - inv = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(self)) - inv['customer_code'] = buyer.accounting_customer_code - inv['issue_date'] = issue_date.strftime('%Y-%m-%d') - inv['transaction_date'] = account_activity.bank_transaction&.paid_at&.strftime('%Y-%m-%d') - inv['language'] = buyer.language == 'en' ? 'ENG' : '' - inv['invoice_lines'] = compose_directo_product + invoice = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(self)) + invoice['customer_code'] = buyer.accounting_customer_code + invoice['issue_date'] = issue_date.strftime('%Y-%m-%d') + invoice['transaction_date'] = account_activity + .bank_transaction&.paid_at&.strftime('%Y-%m-%d') + invoice['language'] = buyer.language == 'en' ? 'ENG' : '' + invoice['invoice_lines'] = compose_directo_product - inv + invoice end def compose_directo_product diff --git a/app/models/concerns/registrar/book_keeping.rb b/app/models/concerns/registrar/book_keeping.rb index c85aac317..27645d2cb 100644 --- a/app/models/concerns/registrar/book_keeping.rb +++ b/app/models/concerns/registrar/book_keeping.rb @@ -10,16 +10,16 @@ module Concerns activities = monthly_activites(month) return unless activities.any? - inv = { + invoice = { 'number': 1, 'customer_code': accounting_customer_code, 'language': language == 'en' ? 'ENG' : '', 'currency': activities.first.currency, 'date': month.end_of_month.strftime('%Y-%m-%d') }.as_json - inv['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) + invoice['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities) - inv + invoice end def prepare_invoice_lines(month:, activities:) From 3e01963b3eb2bd34952523ad70d96c1a4d7cd1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Wed, 18 Mar 2020 19:02:28 +0200 Subject: [PATCH 13/13] Reference master branch of Directo gem --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 80375026d..9ed6b8090 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'domain_name' gem 'haml', '~> 5.0' gem 'wkhtmltopdf-binary' -gem 'directo', github: 'internetee/directo', branch: 'directo-api' +gem 'directo', github: 'internetee/directo', branch: 'master' group :development do # deploy diff --git a/Gemfile.lock b/Gemfile.lock index 3b36f1935..98f999311 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,10 +9,10 @@ GIT GIT remote: https://github.com/internetee/directo.git - revision: 7bac1d5ca413e3cce25a7adad3c0e8d7f8cd673e - branch: directo-api + revision: 8cb63d2fb91c640b264d5af05f4a6afbcfd46979 + branch: master specs: - directo (0.1.0) + directo (1.0.0) money (~> 6.13) nokogiri (~> 1.10)