mirror of
https://github.com/internetee/registry.git
synced 2025-07-25 20:18:22 +02:00
Merge pull request #1676 from internetee/1101-topping-up-credit-account-without-an-invoice
Process payments: Allow direct top up via bank transfer
This commit is contained in:
commit
90b6a0a693
13 changed files with 134 additions and 40 deletions
|
@ -18,7 +18,7 @@ GIT
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/internetee/e_invoice.git
|
remote: https://github.com/internetee/e_invoice.git
|
||||||
revision: b374ffd7be77b559b30c7a0210dc0df5ac3ed723
|
revision: 5f8d0029bf1affdbf2bd6e3d1ce87d34066add4d
|
||||||
branch: master
|
branch: master
|
||||||
specs:
|
specs:
|
||||||
e_invoice (0.1.0)
|
e_invoice (0.1.0)
|
||||||
|
@ -238,7 +238,7 @@ GEM
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
httpi (2.4.4)
|
httpi (2.4.5)
|
||||||
rack
|
rack
|
||||||
socksify
|
socksify
|
||||||
i18n (1.8.5)
|
i18n (1.8.5)
|
||||||
|
@ -467,7 +467,8 @@ GEM
|
||||||
i18n
|
i18n
|
||||||
warden (1.2.8)
|
warden (1.2.8)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.6)
|
||||||
wasabi (3.5.0)
|
wasabi (3.6.1)
|
||||||
|
addressable
|
||||||
httpi (~> 2.0)
|
httpi (~> 2.0)
|
||||||
nokogiri (>= 1.4.2)
|
nokogiri (>= 1.4.2)
|
||||||
webdrivers (4.4.1)
|
webdrivers (4.4.1)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
class SendEInvoiceJob < Que::Job
|
class SendEInvoiceJob < Que::Job
|
||||||
def run(invoice_id)
|
def run(invoice_id, payable: true)
|
||||||
invoice = run_condition(Invoice.find_by(id: invoice_id))
|
invoice = run_condition(Invoice.find_by(id: invoice_id), payable: payable)
|
||||||
|
|
||||||
invoice.to_e_invoice.deliver
|
invoice.to_e_invoice(payable: payable).deliver
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
invoice.update(e_invoice_sent_at: Time.zone.now)
|
invoice.update(e_invoice_sent_at: Time.zone.now)
|
||||||
log_success(invoice)
|
log_success(invoice)
|
||||||
|
@ -15,9 +15,9 @@ class SendEInvoiceJob < Que::Job
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def run_condition(invoice)
|
def run_condition(invoice, payable: true)
|
||||||
destroy unless invoice
|
destroy unless invoice
|
||||||
destroy if invoice.do_not_send_e_invoice?
|
destroy if invoice.do_not_send_e_invoice? && payable
|
||||||
invoice
|
invoice
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,16 +31,18 @@ class BankTransaction < ApplicationRecord
|
||||||
@registrar ||= Invoice.find_by(reference_no: parsed_ref_number)&.buyer
|
@registrar ||= Invoice.find_by(reference_no: parsed_ref_number)&.buyer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def autobindable?
|
||||||
|
!binded? && registrar && invoice.payable? ? true : false
|
||||||
|
rescue NoMethodError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
# For successful binding, reference number, invoice id and sum must match with the invoice
|
# For successful binding, reference number, invoice id and sum must match with the invoice
|
||||||
def autobind_invoice(manual: false)
|
def autobind_invoice(manual: false)
|
||||||
return if binded?
|
return unless autobindable?
|
||||||
return unless registrar
|
|
||||||
return unless invoice
|
|
||||||
return unless invoice.payable?
|
|
||||||
|
|
||||||
channel = manual ? 'admin_payment' : 'system_payment'
|
channel = manual ? 'admin_payment' : 'system_payment'
|
||||||
create_internal_payment_record(channel: channel, invoice: invoice,
|
create_internal_payment_record(channel: channel, invoice: invoice, registrar: registrar)
|
||||||
registrar: registrar)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_internal_payment_record(channel: nil, invoice:, registrar:)
|
def create_internal_payment_record(channel: nil, invoice:, registrar:)
|
||||||
|
@ -89,12 +91,11 @@ class BankTransaction < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_activity(registrar, invoice)
|
def create_activity(registrar, invoice)
|
||||||
activity = AccountActivity.new(
|
activity = AccountActivity.new(account: registrar.cash_account, bank_transaction: self,
|
||||||
account: registrar.cash_account, bank_transaction: self,
|
invoice: invoice, sum: invoice.subtotal,
|
||||||
invoice: invoice, sum: invoice.subtotal,
|
currency: currency, description: description,
|
||||||
currency: currency, description: description,
|
activity_type: AccountActivity::ADD_CREDIT)
|
||||||
activity_type: AccountActivity::ADD_CREDIT
|
|
||||||
)
|
|
||||||
if activity.save
|
if activity.save
|
||||||
reset_pending_registrar_balance_reload
|
reset_pending_registrar_balance_reload
|
||||||
true
|
true
|
||||||
|
@ -103,6 +104,10 @@ class BankTransaction < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parsed_ref_number
|
||||||
|
reference_no || ref_number_from_description
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_pending_registrar_balance_reload
|
def reset_pending_registrar_balance_reload
|
||||||
|
@ -112,17 +117,12 @@ class BankTransaction < ApplicationRecord
|
||||||
registrar.save!
|
registrar.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def parsed_ref_number
|
|
||||||
reference_no || ref_number_from_description
|
|
||||||
end
|
|
||||||
|
|
||||||
def ref_number_from_description
|
def ref_number_from_description
|
||||||
(Billing::ReferenceNo::MULTI_REGEXP.match(description) || []).captures.each do |match|
|
matches = description.to_s.scan(Billing::ReferenceNo::MULTI_REGEXP).flatten
|
||||||
break match if match.length == 7 || valid_ref_no?(match)
|
matches.detect { |m| break m if m.length == 7 || valid_ref_no?(m) }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_ref_no?(match)
|
def valid_ref_no?(match)
|
||||||
return true if Billing::ReferenceNo.valid?(match) && Registrar.find_by(reference_no: match).any?
|
return true if Billing::ReferenceNo.valid?(match) && Registrar.find_by(reference_no: match)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,8 +99,8 @@ class Invoice < ApplicationRecord
|
||||||
generator.as_pdf
|
generator.as_pdf
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_e_invoice
|
def to_e_invoice(payable: true)
|
||||||
generator = Invoice::EInvoiceGenerator.new(self)
|
generator = Invoice::EInvoiceGenerator.new(self, payable: payable)
|
||||||
generator.generate
|
generator.generate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -112,6 +112,15 @@ class Invoice < ApplicationRecord
|
||||||
e_invoice_sent_at.present?
|
e_invoice_sent_at.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.create_from_transaction!(transaction)
|
||||||
|
registrar_user = Registrar.find_by(reference_no: transaction.parsed_ref_number)
|
||||||
|
return unless registrar_user
|
||||||
|
|
||||||
|
vat = VatRateCalculator.new(registrar: registrar_user).calculate
|
||||||
|
net = (transaction.sum / (1 + (vat / 100)))
|
||||||
|
registrar_user.issue_prepayment_invoice(net, 'Direct top-up via bank transfer', payable: false)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def apply_default_buyer_vat_no
|
def apply_default_buyer_vat_no
|
||||||
|
@ -119,6 +128,6 @@ class Invoice < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_total
|
def calculate_total
|
||||||
self.total = subtotal + vat_amount
|
self.total = (subtotal + vat_amount).round(3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
class Invoice
|
class Invoice
|
||||||
class EInvoiceGenerator
|
class EInvoiceGenerator
|
||||||
attr_reader :invoice
|
attr_reader :invoice
|
||||||
|
attr_reader :payable
|
||||||
|
|
||||||
def initialize(invoice)
|
def initialize(invoice, payable)
|
||||||
@invoice = invoice
|
@invoice = invoice
|
||||||
|
@payable = payable
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
|
@ -70,9 +72,10 @@ class Invoice
|
||||||
i.total = invoice.total
|
i.total = invoice.total
|
||||||
i.currency = invoice.currency
|
i.currency = invoice.currency
|
||||||
i.delivery_channel = %i[internet_bank portal]
|
i.delivery_channel = %i[internet_bank portal]
|
||||||
|
i.payable = payable
|
||||||
end
|
end
|
||||||
|
|
||||||
EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice)
|
EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class InvoiceItem < ApplicationRecord
|
||||||
delegate :vat_rate, to: :invoice
|
delegate :vat_rate, to: :invoice
|
||||||
|
|
||||||
def item_sum_without_vat
|
def item_sum_without_vat
|
||||||
(price * quantity).round(2)
|
(price * quantity).round(3)
|
||||||
end
|
end
|
||||||
alias_method :subtotal, :item_sum_without_vat
|
alias_method :subtotal, :item_sum_without_vat
|
||||||
|
|
||||||
|
@ -14,6 +14,6 @@ class InvoiceItem < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def total
|
def total
|
||||||
subtotal + vat_amount
|
(subtotal + vat_amount)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Registrar < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def issue_prepayment_invoice(amount, description = nil)
|
def issue_prepayment_invoice(amount, description = nil, payable: true)
|
||||||
vat_rate = ::Invoice::VatRateCalculator.new(registrar: self).calculate
|
vat_rate = ::Invoice::VatRateCalculator.new(registrar: self).calculate
|
||||||
|
|
||||||
invoice = invoices.create!(
|
invoice = invoices.create!(
|
||||||
|
@ -99,7 +99,12 @@ class Registrar < ApplicationRecord
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
SendEInvoiceJob.enqueue(invoice.id)
|
|
||||||
|
unless payable
|
||||||
|
InvoiceMailer.invoice_email(invoice: invoice, recipient: billing_email).deliver_now
|
||||||
|
end
|
||||||
|
|
||||||
|
SendEInvoiceJob.enqueue(invoice.id, payable: payable)
|
||||||
|
|
||||||
invoice
|
invoice
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class ChangeInvoiceItemPriceScaleToThreePlaces < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
change_column :invoice_items, :price, :decimal, precision: 10, scale: 3
|
||||||
|
end
|
||||||
|
end
|
|
@ -962,7 +962,7 @@ CREATE TABLE public.invoice_items (
|
||||||
description character varying NOT NULL,
|
description character varying NOT NULL,
|
||||||
unit character varying NOT NULL,
|
unit character varying NOT NULL,
|
||||||
quantity integer NOT NULL,
|
quantity integer NOT NULL,
|
||||||
price numeric(10,2) NOT NULL,
|
price numeric(10,3) NOT NULL,
|
||||||
created_at timestamp without time zone,
|
created_at timestamp without time zone,
|
||||||
updated_at timestamp without time zone,
|
updated_at timestamp without time zone,
|
||||||
creator_str character varying,
|
creator_str character varying,
|
||||||
|
@ -4850,6 +4850,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||||
('20200811074839'),
|
('20200811074839'),
|
||||||
('20200812090409'),
|
('20200812090409'),
|
||||||
('20200812125810'),
|
('20200812125810'),
|
||||||
('20200908131554');
|
('20200908131554'),
|
||||||
|
('20200910085157');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,8 @@ namespace :invoices do
|
||||||
reference_no: incoming_transaction.payment_reference_number,
|
reference_no: incoming_transaction.payment_reference_number,
|
||||||
description: incoming_transaction.payment_description }
|
description: incoming_transaction.payment_description }
|
||||||
transaction = bank_statement.bank_transactions.create!(transaction_attributes)
|
transaction = bank_statement.bank_transactions.create!(transaction_attributes)
|
||||||
|
Invoice.create_from_transaction!(transaction) unless transaction.autobindable?
|
||||||
|
|
||||||
transaction.autobind_invoice
|
transaction.autobind_invoice
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -156,6 +156,24 @@ class BankTransactionTest < ActiveSupport::TestCase
|
||||||
assert transaction.errors.full_messages.include?('Cannot bind cancelled invoice')
|
assert transaction.errors.full_messages.include?('Cannot bind cancelled invoice')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_assumes_7_digit_number_is_reference_no_in_desc
|
||||||
|
statement = BankTransaction.new
|
||||||
|
statement.description = 'number 1234567 defo valid'
|
||||||
|
assert_equal '1234567', statement.parsed_ref_number
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_determines_correct_ref_no_from_description
|
||||||
|
statement = BankTransaction.new
|
||||||
|
ref_no = registrars(:bestnames).reference_no
|
||||||
|
statement.description = "invoice 123 125 55 4521 #{ref_no} 7541 defo valid"
|
||||||
|
assert_equal ref_no.to_s, statement.parsed_ref_number
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parsed_ref_no_returns_nil_if_ref_not_found
|
||||||
|
statement = BankTransaction.new
|
||||||
|
statement.description = "all invalid 12 123 55 77777 --"
|
||||||
|
assert_nil statement.parsed_ref_number
|
||||||
|
end
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_payable_invoice(attributes)
|
def create_payable_invoice(attributes)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class InvoiceTest < ActiveSupport::TestCase
|
class InvoiceTest < ActiveSupport::TestCase
|
||||||
|
include ActionMailer::TestHelper
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@invoice = invoices(:one)
|
@invoice = invoices(:one)
|
||||||
end
|
end
|
||||||
|
@ -109,4 +111,33 @@ class InvoiceTest < ActiveSupport::TestCase
|
||||||
seller_zip: nil)
|
seller_zip: nil)
|
||||||
assert_equal 'street, city, state', invoice.seller_address
|
assert_equal 'street, city, state', invoice.seller_address
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_creates_invoice_with_bank_transaction_total
|
||||||
|
registrar = registrars(:bestnames)
|
||||||
|
transaction = bank_transactions(:one).dup
|
||||||
|
transaction.reference_no = registrar.reference_no
|
||||||
|
transaction.sum = 250
|
||||||
|
|
||||||
|
invoice = Invoice.create_from_transaction!(transaction)
|
||||||
|
assert_equal 250, invoice.total
|
||||||
|
|
||||||
|
transaction.sum = 146.88
|
||||||
|
invoice = Invoice.create_from_transaction!(transaction)
|
||||||
|
assert_equal 146.88, invoice.total
|
||||||
|
|
||||||
|
transaction.sum = 0.99
|
||||||
|
invoice = Invoice.create_from_transaction!(transaction)
|
||||||
|
assert_equal 0.99, invoice.total
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_emails_invoice_after_creating_topup_invoice
|
||||||
|
registrar = registrars(:bestnames)
|
||||||
|
transaction = bank_transactions(:one).dup
|
||||||
|
transaction.reference_no = registrar.reference_no
|
||||||
|
transaction.sum = 250
|
||||||
|
|
||||||
|
assert_emails 1 do
|
||||||
|
Invoice.create_from_transaction!(transaction)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -82,6 +82,25 @@ class ProcessPaymentsTaskTest < ActiveSupport::TestCase
|
||||||
assert payment_order.failed?
|
assert payment_order.failed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_credits_registrar_account_without_invoice_beforehand
|
||||||
|
registrar = registrars(:bestnames)
|
||||||
|
|
||||||
|
assert_changes -> { registrar.accounts.first.balance } do
|
||||||
|
run_task
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_changes -> { registrar.invoices.count } do
|
||||||
|
run_task
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_topup_creates_invoice_with_total_of_transactioned_amount
|
||||||
|
registrar = registrars(:bestnames)
|
||||||
|
run_task
|
||||||
|
|
||||||
|
assert_equal 0.1, registrar.invoices.last.total
|
||||||
|
end
|
||||||
|
|
||||||
def test_output
|
def test_output
|
||||||
assert_output "Transactions processed: 1\n" do
|
assert_output "Transactions processed: 1\n" do
|
||||||
run_task
|
run_task
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue