Add balance auto reload

Closes #329
This commit is contained in:
Artur Beljajev 2018-09-06 12:09:57 +03:00
parent 19f9a4eb71
commit 62c38d1f99
29 changed files with 660 additions and 16 deletions

View file

@ -90,6 +90,7 @@ gem 'active_model-errors_details' # Backport from Rails 5, https://github.com/ra
gem 'airbrake'
gem 'company_register', github: 'internetee/company_register', branch: :master
gem 'e_invoice', github: 'internetee/e_invoice', branch: :master
group :development do
# deploy

View file

@ -15,6 +15,16 @@ GIT
data_migrate (1.3.0)
rails (>= 4.1.0)
GIT
remote: https://github.com/internetee/e_invoice.git
revision: 7817bbefd4ed0b140ed781172bd64a856b622273
branch: master
specs:
e_invoice (0.1.0)
builder (~> 3.2)
nokogiri
savon
GIT
remote: https://github.com/internetee/epp-xml.git
revision: 5dd542e67ef26d58365f30e553254d6db809277d
@ -456,6 +466,7 @@ DEPENDENCIES
database_cleaner
devise (~> 4.0)
digidoc_client!
e_invoice!
epp (= 1.5.0)!
epp-xml (= 1.1.0)!
factory_bot_rails

View file

@ -2,6 +2,7 @@ class Registrar
class AccountController < BaseController
skip_authorization_check
helper_method :iban_max_length
helper_method :balance_auto_reload_setting
def show; end
@ -25,5 +26,9 @@ class Registrar
def iban_max_length
Iban.max_length
end
def balance_auto_reload_setting
current_registrar_user.registrar.settings['balance_auto_reload']
end
end
end

View file

@ -0,0 +1,52 @@
class Registrar
module Settings
class BalanceAutoReloadController < BaseController
before_action :authorize
def edit
@type = if current_registrar.settings['balance_auto_reload']
type_params = current_registrar.settings['balance_auto_reload']['type']
.except('name')
BalanceAutoReloadTypes::Threshold.new(type_params)
else
BalanceAutoReloadTypes::Threshold.new
end
end
def update
type = BalanceAutoReloadTypes::Threshold.new(type_params)
current_registrar.update!(settings: { balance_auto_reload: { type: type } })
redirect_to registrar_account_path, notice: t('.saved')
end
def destroy
current_registrar.settings.delete('balance_auto_reload')
current_registrar.save!
redirect_to registrar_account_path, notice: t('.disabled')
end
private
def type_params
permitted_params = params.require(:type).permit(:amount, :threshold)
normalize_params(permitted_params)
end
def normalize_params(params)
params[:amount] = params[:amount].to_f
params[:threshold] = params[:threshold].to_f
params
end
def authorize
authorize!(:manage, :balance_auto_reload)
end
def current_registrar
current_registrar_user.registrar
end
end
end
end

View file

@ -72,6 +72,7 @@ class Ability
can(:manage, Invoice) { |i| i.buyer_id == @user.registrar_id }
can :manage, :deposit
can :read, AccountActivity
can :manage, :balance_auto_reload
end
def customer_service # Admin/admin_user dynamic role

View file

@ -0,0 +1,25 @@
module BalanceAutoReloadTypes
class Threshold
include ActiveModel::Model
attr_accessor :amount
attr_accessor :threshold
validates :amount, numericality: { greater_than_or_equal_to: :min_amount }
validates :threshold, numericality: { greater_than_or_equal_to: 0 }
def min_amount
Setting.minimum_deposit
end
def as_json(options)
{ name: name }.merge(super)
end
private
def name
self.class.name.demodulize.underscore
end
end
end

View file

@ -79,13 +79,23 @@ class BankTransaction < ActiveRecord::Base
end
def create_activity(registrar, invoice)
create_account_activity(
account: registrar.cash_account,
invoice: invoice,
sum: invoice.subtotal,
currency: currency,
description: description,
activity_type: AccountActivity::ADD_CREDIT
)
ActiveRecord::Base.transaction do
create_account_activity!(account: registrar.cash_account,
invoice: invoice,
sum: invoice.subtotal,
currency: currency,
description: description,
activity_type: AccountActivity::ADD_CREDIT)
reset_pending_registrar_balance_reload
end
end
private
def reset_pending_registrar_balance_reload
return unless registrar.settings['balance_auto_reload']
registrar.settings['balance_auto_reload'].delete('pending')
registrar.save!
end
end

View file

@ -98,6 +98,11 @@ class Invoice < ActiveRecord::Base
generator.as_pdf
end
def to_e_invoice
generator = Invoice::EInvoiceGenerator.new(self)
generator.generate
end
private
def apply_default_buyer_vat_no

View file

@ -0,0 +1,77 @@
class Invoice
class EInvoiceGenerator
attr_reader :invoice
def initialize(invoice)
@invoice = invoice
end
def generate
seller = EInvoice::Seller.new
seller.name = invoice.seller_name
seller.registration_number = invoice.seller_reg_no
seller.vat_number = invoice.seller_vat_no
seller_legal_address = EInvoice::Address.new
seller_legal_address.line1 = invoice.seller_street
seller_legal_address.line2 = invoice.seller_state
seller_legal_address.postal_code = invoice.seller_zip
seller_legal_address.city = invoice.seller_city
seller_legal_address.country = invoice.seller_country
seller.legal_address = seller_legal_address
buyer = EInvoice::Buyer.new
buyer.name = invoice.buyer_name
buyer.registration_number = invoice.buyer_reg_no
buyer.vat_number = invoice.buyer_vat_no
buyer.email = invoice.buyer.billing_email
buyer_bank_account = EInvoice::BankAccount.new
buyer_bank_account.number = invoice.buyer.e_invoice_iban
buyer.bank_account = buyer_bank_account
buyer_legal_address = EInvoice::Address.new
buyer_legal_address.line1 = invoice.buyer_street
buyer_legal_address.line2 = invoice.buyer_state
buyer_legal_address.postal_code = invoice.buyer_zip
buyer_legal_address.city = invoice.buyer_city
buyer_legal_address.country = invoice.buyer_country
buyer.legal_address = buyer_legal_address
e_invoice_invoice_items = []
invoice.each do |invoice_item|
e_invoice_invoice_item = EInvoice::InvoiceItem.new.tap do |i|
i.description = invoice_item.description
i.price = invoice_item.price
i.quantity = invoice_item.quantity
i.unit = invoice_item.unit
i.subtotal = invoice_item.subtotal
i.vat_rate = invoice_item.vat_rate
i.vat_amount = invoice_item.vat_amount
i.total = invoice_item.total
end
e_invoice_invoice_items << e_invoice_invoice_item
end
e_invoice_invoice = EInvoice::Invoice.new.tap do |i|
i.seller = seller
i.buyer = buyer
i.items = e_invoice_invoice_items
i.number = invoice.number
i.date = invoice.issue_date
i.recipient_id_code = invoice.buyer_reg_no
i.reference_number = invoice.reference_no
i.due_date = invoice.due_date
i.beneficiary_name = invoice.seller_name
i.beneficiary_account_number = invoice.seller_iban
i.payer_name = invoice.buyer_name
i.subtotal = invoice.subtotal
i.vat_amount = invoice.vat_amount
i.total = invoice.total
i.currency = invoice.currency
end
EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice)
end
end
end

View file

@ -165,6 +165,10 @@ class Registrar < ActiveRecord::Base
notifications.create!(text: text)
end
def e_invoice_iban
iban
end
private
def set_defaults

View file

@ -0,0 +1,30 @@
<div class="panel panel-default balance-auto-reload">
<div class="panel-heading">
<%= t '.header' %>
</div>
<div class="panel-body">
<% if setting %>
<span class="label label-success"><%= t '.enabled' %></span>
<%= t '.enabled_state_details', amount: number_to_currency(setting['type']['amount']),
threshold: number_to_currency(setting['type']['threshold']) %>
<% else %>
<span class="text-muted"><%= t '.disabled' %></span>
<% end %>
</div>
<div class="panel-footer text-right">
<% if !setting %>
<%= link_to t('.enable_btn'), edit_registrar_settings_balance_auto_reload_path,
class: 'btn btn-default btn-sm' %>
<% else %>
<%= link_to t('.disable_btn'), registrar_settings_balance_auto_reload_path,
method: :delete, class: 'btn btn-default btn-sm' %>
<% end %>
<% if setting %>
<%= link_to t('.edit_btn'), edit_registrar_settings_balance_auto_reload_path,
class: 'btn btn-default btn-sm' %>
<% end %>
</div>
</div>

View file

@ -12,4 +12,12 @@
<div class="col-sm-6">
<%= render 'linked_users', linked_users: current_registrar_user.linked_users %>
</div>
</div>
</div>
<% if can?(:manage, :balance_auto_reload) %>
<div class="row">
<div class="col-sm-6">
<%= render 'balance_auto_reload', setting: balance_auto_reload_setting %>
</div>
</div>
<% end %>

View file

@ -0,0 +1 @@
<%= render 'registrar/settings/balance_auto_reload/form/types/threshold', type: @type %>

View file

@ -0,0 +1,10 @@
<ol class="breadcrumb">
<li><%= link_to t('registrar.account.show.header'), registrar_account_path %></li>
<li><%= t '.header' %></li>
</ol>
<div class="page-header">
<h1><%= t '.header' %></h1>
</div>
<%= render 'form' %>

View file

@ -0,0 +1,40 @@
<p><%= t '.description' %></p>
<%= form_for type, as: :type, url: registrar_settings_balance_auto_reload_path, method: :patch,
html: { class: 'form-horizontal' } do |f| %>
<%= render 'form_errors', target: type %>
<div class="form-group">
<%= f.label :amount, class: 'col-md-2 control-label' %>
<div class="col-md-2">
<div class="input-group">
<%= f.money_field :amount, required: true, autofocus: true, class: 'form-control' %>
<div class="input-group-addon"><%= Money::default_currency.symbol %></div>
</div>
</div>
<div class="col-md-4">
<span class="help-block"><%= t '.amount_hint', min_amount: f.object.min_amount %></span>
</div>
</div>
<div class="form-group">
<%= f.label :threshold, class: 'col-md-2 control-label' %>
<div class="col-md-2">
<div class="input-group">
<%= f.money_field :threshold, required: true, class: 'form-control' %>
<div class="input-group-addon"><%= Money::default_currency.symbol %></div>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="col-md-offset-3 col-md-2">
<%= f.submit t('.submit_btn'), class: 'btn btn-success' %>
</div>
</div>
<% end %>

View file

@ -135,6 +135,10 @@ payments_every_pay_seller_account: 'EUR3D1'
payments_every_pay_api_user: 'api_user'
payments_every_pay_api_key: 'api_key'
e_invoice_provider_name: 'omniva'
e_invoice_provider_password:
e_invoice_provider_test_mode: 'false'
user_session_timeout: '3600' # 1 hour
secure_session_cookies: 'false' # true|false
same_site_session_cookies: 'false' # false|strict|lax

View file

@ -0,0 +1,3 @@
provider_config = { password: ENV['e_invoice_provider_password'],
test_mode: ENV['e_invoice_provider_test_mode'] == 'true' }
EInvoice.provider = EInvoice::Providers::OmnivaProvider.new(provider_config)

View file

@ -21,3 +21,12 @@ en:
linked_users:
header: Linked users
switch_btn: Switch
balance_auto_reload:
header: Balance Auto-Reload
enabled: Enabled
enabled_state_details: Reload %{amount} when your balance drops to %{threshold}
disabled: Disabled
enable_btn: Enable
disable_btn: Disable
edit_btn: Edit

View file

@ -0,0 +1,19 @@
en:
registrar:
settings:
balance_auto_reload:
edit:
header: Balance Auto-Reload
form:
types:
threshold:
description: Automatically reload your balance when it drops to a minimum threshold
amount_hint: must be greater than or equal to %{min_amount}
submit_btn: Save
update:
saved: Balance Auto-Reload setting has been updated
destroy:
disabled: Balance Auto-Reload setting has been disabled

View file

@ -115,6 +115,10 @@ Rails.application.routes.draw do
put 'pay/return/:bank' => 'payments#back'
post 'pay/callback/:bank' => 'payments#callback', as: 'response_payment_with'
get 'pay/go/:bank' => 'payments#pay', as: 'payment_with'
namespace :settings do
resource :balance_auto_reload, controller: :balance_auto_reload, only: %i[edit update destroy]
end
end
scope :registrar do

View file

@ -0,0 +1,5 @@
class AddRegistrarsSettings < ActiveRecord::Migration
def change
add_column :registrars, :settings, :jsonb, null: false, default: '{}'
end
end

View file

@ -2201,7 +2201,8 @@ CREATE TABLE public.registrars (
test_registrar boolean DEFAULT false,
language character varying NOT NULL,
vat_rate numeric(4,3),
iban character varying
iban character varying,
settings jsonb DEFAULT '{}'::jsonb NOT NULL
);
@ -4963,6 +4964,8 @@ INSERT INTO schema_migrations (version) VALUES ('20190328151838');
INSERT INTO schema_migrations (version) VALUES ('20190415120246');
INSERT INTO schema_migrations (version) VALUES ('20190426174225');
INSERT INTO schema_migrations (version) VALUES ('20190510090240');
INSERT INTO schema_migrations (version) VALUES ('20190510102549');

View file

@ -0,0 +1,35 @@
namespace :registrars do
desc 'Reloads balance of registrars'
task reload_balance: :environment do
include ActionView::Helpers::NumberHelper
invoiced_registrar_count = 0
Registrar.transaction do
Registrar.all.each do |registrar|
balance_auto_reload_setting = registrar.settings['balance_auto_reload']
next unless balance_auto_reload_setting
reload_pending = balance_auto_reload_setting['pending']
threshold_reached = registrar.balance <= balance_auto_reload_setting['type']['threshold']
reload_amount = balance_auto_reload_setting['type']['amount']
next if reload_pending || !threshold_reached
Registrar.transaction do
invoice = registrar.issue_prepayment_invoice(reload_amount)
e_invoice = invoice.to_e_invoice
e_invoice.deliver
registrar.settings['balance_auto_reload']['pending'] = true
registrar.save!
end
puts %(Registrar "#{registrar}" got #{number_to_currency(reload_amount, unit: 'EUR')})
invoiced_registrar_count += 1
end
end
puts "Invoiced total: #{invoiced_registrar_count}"
end
end

View file

@ -0,0 +1,32 @@
require 'test_helper'
class RegistrarAreaSettingsBalanceAutoReloadIntegrationTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
@registrar = registrars(:bestnames)
sign_in users(:api_bestnames)
end
def test_updates_balance_auto_reload_setting
amount = 100
threshold = 10
assert_nil @registrar.settings['balance_auto_reload']
patch registrar_settings_balance_auto_reload_path, { type: { amount: amount,
threshold: threshold } }
@registrar.reload
assert_equal amount, @registrar.settings['balance_auto_reload']['type']['amount']
assert_equal threshold, @registrar.settings['balance_auto_reload']['type']['threshold']
end
def test_disables_balance_auto_reload_setting
@registrar.update!(settings: { balance_auto_reload: { amount: 'any', threshold: 'any' } })
delete registrar_settings_balance_auto_reload_path
@registrar.reload
assert_nil @registrar.settings['balance_auto_reload']
end
end

View file

@ -0,0 +1,62 @@
require 'test_helper'
class BalanceAutoReloadTypes::ThresholdTest < ActiveSupport::TestCase
setup do
@original_min_reload_amount = Setting.minimum_deposit
end
teardown do
Setting.minimum_deposit = @original_min_reload_amount
end
def test_valid_fixture_is_valid
assert valid_type.valid?
end
def test_invalid_without_amount
type = valid_type
type.amount = nil
assert type.invalid?
end
def test_invalid_when_amount_is_smaller_than_required_minimum
type = valid_type
Setting.minimum_deposit = 0.02
type.amount = 0.01
assert type.invalid?
end
def test_valid_when_amount_equals_allowed_minimum
type = valid_type
Setting.minimum_deposit = 0.02
type.amount = 0.02
assert type.valid?
end
def test_invalid_without_threshold
type = valid_type
type.threshold = nil
assert type.invalid?
end
def test_invalid_when_threshold_is_less_than_zero
type = valid_type
type.threshold = -1
assert type.invalid?
end
def test_serializes_to_json
type = BalanceAutoReloadTypes::Threshold.new(amount: 100, threshold: 10)
assert_equal ({ name: 'threshold', amount: 100, threshold: 10 }).to_json, type.to_json
end
private
def valid_type
BalanceAutoReloadTypes::Threshold.new(amount: Setting.minimum_deposit, threshold: 0)
end
end

View file

@ -1,8 +1,13 @@
require 'test_helper'
class BankTransactionTest < ActiveSupport::TestCase
def test_matches_against_invoice_reference_number
invoices(:one).update!(account_activity: nil, number: '2222', total: 10, reference_no: '1111')
setup do
@registrar = registrars(:bestnames)
@invoice = invoices(:one)
end
def test_matches_against_invoice_number_and_reference_number
create_payable_invoice(number: '2222', total: 10, reference_no: '1111')
transaction = BankTransaction.new(description: 'invoice #2222', sum: 10, reference_no: '1111')
assert_difference 'AccountActivity.count' do
@ -10,8 +15,19 @@ class BankTransactionTest < ActiveSupport::TestCase
end
end
def test_resets_pending_registrar_balance_reload
registrar = registrar_with_pending_balance_auto_reload
create_payable_invoice(number: '2222', total: 10, reference_no: '1111')
transaction = BankTransaction.new(description: 'invoice #2222', sum: 10, reference_no: '1111')
transaction.autobind_invoice
registrar.reload
assert_nil registrar.settings['balance_auto_reload']['pending']
end
def test_does_not_match_against_registrar_reference_number
registrars(:bestnames).update!(reference_no: '1111')
@registrar.update!(reference_no: '1111')
transaction = BankTransaction.new(description: 'invoice #2222', sum: 10, reference_no: '1111')
assert_no_difference 'AccountActivity.count' do
@ -20,7 +36,7 @@ class BankTransactionTest < ActiveSupport::TestCase
end
def test_underpayment_is_not_matched_with_invoice
invoices(:one).update!(account_activity: nil, number: '2222', total: 10)
create_payable_invoice(number: '2222', total: 10)
transaction = BankTransaction.new(sum: 9)
assert_no_difference 'AccountActivity.count' do
@ -30,7 +46,7 @@ class BankTransactionTest < ActiveSupport::TestCase
end
def test_overpayment_is_not_matched_with_invoice
invoices(:one).update!(account_activity: nil, number: '2222', total: 10)
create_payable_invoice(number: '2222', total: 10)
transaction = BankTransaction.new(sum: 11)
assert_no_difference 'AccountActivity.count' do
@ -40,7 +56,7 @@ class BankTransactionTest < ActiveSupport::TestCase
end
def test_cancelled_invoice_is_not_matched
invoices(:one).update!(account_activity: nil, number: '2222', total: 10, cancelled_at: '2010-07-05')
@invoice.update!(account_activity: nil, number: '2222', total: 10, cancelled_at: '2010-07-05')
transaction = BankTransaction.new(sum: 10)
assert_no_difference 'AccountActivity.count' do
@ -48,4 +64,17 @@ class BankTransactionTest < ActiveSupport::TestCase
end
assert transaction.errors.full_messages.include?('Cannot bind cancelled invoice')
end
private
def create_payable_invoice(attributes)
payable_attributes = { account_activity: nil }
@invoice.update!(payable_attributes.merge(attributes))
@invoice
end
def registrar_with_pending_balance_auto_reload
@registrar.update!(settings: { balance_auto_reload: { pending: true } })
@registrar
end
end

View file

@ -176,6 +176,12 @@ class RegistrarTest < ActiveSupport::TestCase
assert_equal vat_country, registrar.vat_country
end
def test_returns_iban_for_e_invoice_delivery_channel
iban = 'GB33BUKB20201555555555'
registrar = Registrar.new(iban: iban)
assert_equal iban, registrar.e_invoice_iban
end
private
def valid_registrar

View file

@ -0,0 +1,74 @@
require 'test_helper'
class RegistrarAreaSettingsBalanceAutoReloadTest < ApplicationSystemTestCase
setup do
@registrar = registrars(:bestnames)
@user = users(:api_bestnames)
sign_in @user
end
def test_enables_balance_auto_reload
amount = 100
threshold = 10
assert_nil @registrar.settings['balance_auto_reload']
visit registrar_account_path
click_on 'Enable'
fill_in 'Amount', with: amount
fill_in 'Threshold', with: threshold
click_button 'Save'
assert_current_path registrar_account_path
assert_text 'Balance Auto-Reload setting has been updated'
# Using `number_to_currency` leads to `expected to find text "Reload 100,00 € when your balance
# drops to 10,00 €" in "...Reload 100,00 € when your balance drops to 10,00 €...`
assert_text 'Reload 100,00 € when your balance drops to 10,00 €'
end
def test_disables_balance_auto_reload
@registrar.update!(settings: { balance_auto_reload: { type: {} } })
visit registrar_account_path
click_on 'Disable'
assert_current_path registrar_account_path
assert_text 'Balance Auto-Reload setting has been disabled'
end
def test_edits_balance_auto_reload
@registrar.update!(settings: { balance_auto_reload: { type: { name: 'threshold',
amount: 100,
threshold: 10 } } })
visit registrar_account_path
within '.balance-auto-reload' do
click_on 'Edit'
end
fill_in 'Amount', with: '101'
fill_in 'Threshold', with: '11'
click_button 'Save'
assert_current_path registrar_account_path
assert_text 'Balance Auto-Reload setting has been updated'
end
def test_form_is_pre_populated_when_editing
amount = 100
threshold = 10
@registrar.update!(settings: { balance_auto_reload: { type: { name: 'threshold',
amount: amount,
threshold: threshold } } })
visit edit_registrar_settings_balance_auto_reload_path
assert_field 'Amount', with: amount
assert_field 'Threshold', with: threshold
end
def test_user_of_epp_role_cannot_edit_balance_auto_reload_setting
@user.update!(roles: [ApiUser::EPP])
visit registrar_account_path
assert_no_text 'Balance Auto-Reload'
end
end

View file

@ -0,0 +1,79 @@
require 'test_helper'
class ReloadBalanceTaskTest < ActiveSupport::TestCase
include ActionView::Helpers::NumberHelper
setup do
@registrar = registrars(:bestnames)
EInvoice.provider = EInvoice::Providers::TestProvider.new
end
def test_issues_invoice_when_auto_reload_is_enabled_and_threshold_reached
reload_amount = 100
registrar = registrar_with_auto_reload_enabled_and_threshold_reached(reload_amount)
assert_difference -> { registrar.invoices.count } do
capture_io { run_task }
end
invoice = registrar.invoices.last
assert_equal reload_amount, invoice.subtotal
end
def test_skips_issuing_invoice_when_threshold_is_not_reached
registrar = registrar_with_auto_reload_enabled_and_threshold_not_reached
assert_no_difference -> { registrar.invoices.count } do
capture_io { run_task }
end
end
def test_skips_issuing_invoice_when_balance_reload_is_pending
registrar = registrar_with_auto_reload_enabled_and_threshold_reached
registrar.settings['balance_auto_reload']['pending'] = true
registrar.save!
assert_no_difference -> { registrar.invoices.count } do
capture_io { run_task }
end
end
def test_marks_registrar_as_pending_balance_reload
registrar = registrar_with_auto_reload_enabled_and_threshold_reached
capture_io { run_task }
registrar.reload
assert registrar.settings['balance_auto_reload']['pending']
end
def test_output
reload_amount = 100
registrar = registrar_with_auto_reload_enabled_and_threshold_reached(reload_amount)
assert_equal 'Best Names', registrar.name
assert_output %(Registrar "Best Names" got #{number_to_currency(reload_amount, unit: 'EUR')}\nInvoiced total: 1\n) do
run_task
end
end
private
def registrar_with_auto_reload_enabled_and_threshold_reached(reload_amount = 100)
auto_reload_type = BalanceAutoReloadTypes::Threshold.new(amount: reload_amount, threshold: 10)
@registrar.update!(settings: { balance_auto_reload: { type: auto_reload_type } })
@registrar.accounts.first.update!(balance: 10)
@registrar
end
def registrar_with_auto_reload_enabled_and_threshold_not_reached
auto_reload_type = BalanceAutoReloadTypes::Threshold.new(amount: 100, threshold: 10)
@registrar.update!(settings: { balance_auto_reload: { type: auto_reload_type } })
@registrar.accounts.first.update!(balance: 11)
@registrar
end
def run_task
Rake::Task['registrars:reload_balance'].execute
end
end