diff --git a/app/controllers/admin/registrars_controller.rb b/app/controllers/admin/registrars_controller.rb
index 1a92e67ed..1e9df288b 100644
--- a/app/controllers/admin/registrars_controller.rb
+++ b/app/controllers/admin/registrars_controller.rb
@@ -2,6 +2,7 @@ module Admin
class RegistrarsController < BaseController
load_and_authorize_resource
before_action :set_registrar, only: [:show, :edit, :update, :destroy]
+ helper_method :registry_vat_rate
def index
@q = Registrar.joins(:accounts).ordered.search(params[:q])
@@ -74,5 +75,9 @@ module Admin
:billing_email,
:language)
end
+
+ def registry_vat_rate
+ Registry.current.vat_rate
+ end
end
end
diff --git a/app/models/invoice.rb b/app/models/invoice.rb
index 7e4dfe392..7a8512e28 100644
--- a/app/models/invoice.rb
+++ b/app/models/invoice.rb
@@ -28,16 +28,12 @@ class Invoice < ActiveRecord::Base
validates :due_date, :currency, :seller_name,
:seller_iban, :buyer_name, :items, presence: true
- validates :vat_rate, numericality: { greater_than_or_equal_to: 0, less_than: 100 },
- allow_nil: true
before_create :set_invoice_number
- before_create :apply_default_vat_rate, unless: :vat_rate?
before_create :calculate_total, unless: :total?
before_create :apply_default_buyer_vat_no, unless: :buyer_vat_no?
attribute :vat_rate, ::Type::VATRate.new
- attr_readonly :vat_rate
def set_invoice_number
last_no = Invoice.order(number: :desc).where('number IS NOT NULL').limit(1).pluck(:number).first
@@ -85,7 +81,6 @@ class Invoice < ActiveRecord::Base
end
def vat_amount
- return 0 unless vat_rate
subtotal * vat_rate / 100
end
@@ -105,10 +100,6 @@ class Invoice < ActiveRecord::Base
private
- def apply_default_vat_rate
- self.vat_rate = buyer.effective_vat_rate
- end
-
def apply_default_buyer_vat_no
self.buyer_vat_no = buyer.vat_no
end
diff --git a/app/models/invoice/vat_rate_calculator.rb b/app/models/invoice/vat_rate_calculator.rb
new file mode 100644
index 000000000..a1339e762
--- /dev/null
+++ b/app/models/invoice/vat_rate_calculator.rb
@@ -0,0 +1,19 @@
+class Invoice
+ class VatRateCalculator
+ attr_reader :registry
+ attr_reader :registrar
+
+ def initialize(registry: Registry.current, registrar:)
+ @registry = registry
+ @registrar = registrar
+ end
+
+ def calculate
+ if registrar.vat_liable_locally?(registry)
+ registry.vat_rate
+ else
+ registrar.vat_rate || 0
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/registrar.rb b/app/models/registrar.rb
index 1d2f2b4bd..8cf46ff56 100644
--- a/app/models/registrar.rb
+++ b/app/models/registrar.rb
@@ -22,9 +22,9 @@ class Registrar < ActiveRecord::Base
validates :reference_no, format: Billing::ReferenceNo::REGEXP
validate :forbid_special_code
- validates :vat_rate, presence: true, if: 'foreign_vat_payer? && vat_no.blank?'
- validates :vat_rate, absence: true, if: :home_vat_payer?
- validates :vat_rate, absence: true, if: 'foreign_vat_payer? && vat_no?'
+ validates :vat_rate, presence: true, if: 'vat_liable_in_foreign_country? && vat_no.blank?'
+ validates :vat_rate, absence: true, if: :vat_liable_locally?
+ validates :vat_rate, absence: true, if: 'vat_liable_in_foreign_country? && vat_no?'
validates :vat_rate, numericality: { greater_than_or_equal_to: 0, less_than: 100 },
allow_nil: true
@@ -52,7 +52,9 @@ class Registrar < ActiveRecord::Base
end
def issue_prepayment_invoice(amount, description = nil)
- invoices.create(
+ vat_rate = ::Invoice::VatRateCalculator.new(registrar: self).calculate
+
+ invoices.create!(
issue_date: Time.zone.today,
due_date: (Time.zone.now + Setting.days_to_keep_invoices_active.days).to_date,
description: description,
@@ -84,6 +86,7 @@ class Registrar < ActiveRecord::Base
buyer_url: website,
buyer_email: email,
reference_no: reference_no,
+ vat_rate: vat_rate,
items_attributes: [
{
description: 'prepayment',
@@ -146,12 +149,16 @@ class Registrar < ActiveRecord::Base
end
end
- def effective_vat_rate
- if home_vat_payer?
- Registry.instance.vat_rate
- else
- vat_rate
- end
+ def vat_country=(country)
+ self.address_country_code = country.alpha2
+ end
+
+ def vat_country
+ country
+ end
+
+ def vat_liable_locally?(registry = Registry.current)
+ vat_country == registry.vat_country
end
def notify(action)
@@ -169,11 +176,7 @@ class Registrar < ActiveRecord::Base
errors.add(:code, :forbidden) if code == 'CID'
end
- def home_vat_payer?
- country == Registry.instance.legal_address_country
+ def vat_liable_in_foreign_country?
+ !vat_liable_locally?
end
-
- def foreign_vat_payer?
- !home_vat_payer?
- end
-end
+end
\ No newline at end of file
diff --git a/app/models/registry.rb b/app/models/registry.rb
index 5e5c8cd93..38037c01b 100644
--- a/app/models/registry.rb
+++ b/app/models/registry.rb
@@ -1,11 +1,13 @@
class Registry
- include Singleton
+ include ActiveModel::Model
- def vat_rate
- Setting.registry_vat_prc.to_d * 100
- end
+ attr_accessor :vat_rate
+ attr_accessor :vat_country
- def legal_address_country
- Country.new(Setting.registry_country_code)
+ def self.current
+ vat_rate = Setting.registry_vat_prc.to_d * 100
+ vat_country = Country.new(Setting.registry_country_code)
+
+ new(vat_rate: vat_rate, vat_country: vat_country)
end
-end
+end
\ No newline at end of file
diff --git a/app/views/admin/registrars/_form.html.erb b/app/views/admin/registrars/_form.html.erb
index 65d18f2e7..5545adef1 100644
--- a/app/views/admin/registrars/_form.html.erb
+++ b/app/views/admin/registrars/_form.html.erb
@@ -60,7 +60,7 @@
<%= render 'admin/registrars/form/address', f: f %>
- <%= render 'admin/registrars/form/billing', f: f %>
+ <%= render 'admin/registrars/form/billing', f: f, registry_vat_rate: registry_vat_rate %>
diff --git a/app/views/admin/registrars/form/_address.html.erb b/app/views/admin/registrars/form/_address.html.erb
index 5956e1c8c..d0adfa858 100644
--- a/app/views/admin/registrars/form/_address.html.erb
+++ b/app/views/admin/registrars/form/_address.html.erb
@@ -51,6 +51,7 @@
<%= f.select :address_country_code,
SortedCountry.all_options(f.object.address_country_code), {},
required: true, class: 'form-control' %>
+ <%= t '.country_hint' %>
diff --git a/app/views/admin/registrars/form/_billing.html.erb b/app/views/admin/registrars/form/_billing.html.erb
index b24009440..e9f9b5c88 100644
--- a/app/views/admin/registrars/form/_billing.html.erb
+++ b/app/views/admin/registrars/form/_billing.html.erb
@@ -21,12 +21,14 @@
<%= f.label :vat_rate %>
-
-
diff --git a/config/locales/admin/registrars.en.yml b/config/locales/admin/registrars.en.yml
index e08af5ad1..7256fabb2 100644
--- a/config/locales/admin/registrars.en.yml
+++ b/config/locales/admin/registrars.en.yml
@@ -52,10 +52,15 @@ en:
address:
header: Address
- hint: Used as a billing address
+ country_hint: Affects VAT rate calculation
+ hint: This address is used as a billing address
billing:
header: Billing
+ vat_rate_hint: >-
+ Applies to new invoices.
+ Leave blank if a registrar is VAT-registered;
+ registry's rate of %{registry_vat_rate} will be applied in this case.
no_reference_number_hint: Reference number will be generated automatically
disabled_reference_number_hint: Reference number cannot be changed
diff --git a/config/locales/registrars.en.yml b/config/locales/registrars.en.yml
index 60cb9d5c5..609f9f94a 100644
--- a/config/locales/registrars.en.yml
+++ b/config/locales/registrars.en.yml
@@ -6,3 +6,6 @@ en:
attributes:
code:
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
diff --git a/lib/tasks/data_migrations/populate_invoice_vat_rate.rake b/lib/tasks/data_migrations/populate_invoice_vat_rate.rake
new file mode 100644
index 000000000..3bef60c0d
--- /dev/null
+++ b/lib/tasks/data_migrations/populate_invoice_vat_rate.rake
@@ -0,0 +1,16 @@
+namespace :data_migrations do
+ task populate_invoice_vat_rate: :environment do
+ processed_invoice_count = 0
+
+ Invoice.transaction do
+ Invoice.where(vat_rate: nil).find_each do |invoice|
+ vat_rate = Invoice::VatRateCalculator.new(registrar: invoice.buyer).calculate
+ invoice.update_columns(vat_rate: vat_rate)
+
+ processed_invoice_count += 1
+ end
+ end
+
+ puts "Invoices processed: #{processed_invoice_count}"
+ end
+end
\ No newline at end of file
diff --git a/test/models/invoice/vat_rate_calculator_test.rb b/test/models/invoice/vat_rate_calculator_test.rb
new file mode 100644
index 000000000..682832c1c
--- /dev/null
+++ b/test/models/invoice/vat_rate_calculator_test.rb
@@ -0,0 +1,32 @@
+require 'test_helper'
+
+class Invoice::VatRateCalculatorTest < ActiveSupport::TestCase
+ def test_applies_registry_vat_rate_when_registrar_is_vat_liable_locally
+ registry_vat_rate = 20
+ registry = Registry.new(vat_rate: registry_vat_rate, vat_country: Country.new(:us))
+ registrar = Registrar.new(vat_rate: 10, vat_country: Country.new(:us))
+
+ vat_calculator = Invoice::VatRateCalculator.new(registry: registry, registrar: registrar)
+
+ assert_equal registry_vat_rate, vat_calculator.calculate
+ end
+
+ def test_applies_registrar_vat_rate_when_registrar_is_vat_liable_in_foreign_country
+ registrar_vat_rate = 20
+ registry = Registry.new(vat_rate: 10, vat_country: Country.new(:gb))
+ registrar = Registrar.new(vat_rate: registrar_vat_rate, vat_country: Country.new(:us))
+
+ vat_calculator = Invoice::VatRateCalculator.new(registry: registry, registrar: registrar)
+
+ assert_equal registrar_vat_rate, vat_calculator.calculate
+ end
+
+ def test_applies_zero_vat_rate_when_registrar_is_vat_liable_in_foreign_country_and_vat_rate_is_absent
+ registry = Registry.new(vat_country: Country.new(:gb))
+ registrar = Registrar.new(vat_rate: nil, vat_country: Country.new(:us))
+
+ vat_calculator = Invoice::VatRateCalculator.new(registry: registry, registrar: registrar)
+
+ assert vat_calculator.calculate.zero?
+ end
+end
\ No newline at end of file
diff --git a/test/models/invoice_test.rb b/test/models/invoice_test.rb
index 56d533458..6e6971139 100644
--- a/test/models/invoice_test.rb
+++ b/test/models/invoice_test.rb
@@ -33,53 +33,11 @@ class InvoiceTest < ActiveSupport::TestCase
assert_not Invoice.overdue.include?(@invoice), 'Should not return non-overdue invoice'
end
- def test_optional_vat_rate
- @invoice.vat_rate = nil
- assert @invoice.valid?
- end
-
- def test_vat_rate_validation
- @invoice.vat_rate = -1
- assert @invoice.invalid?
-
- @invoice.vat_rate = 0
- assert @invoice.valid?
-
- @invoice.vat_rate = 99.9
- assert @invoice.valid?
-
- @invoice.vat_rate = 100
- assert @invoice.invalid?
- end
-
def test_serializes_and_deserializes_vat_rate
- invoice = @invoice.dup
- invoice.items = @invoice.items
- invoice.vat_rate = BigDecimal('25.5')
- invoice.save!
- invoice.reload
- assert_equal BigDecimal('25.5'), invoice.vat_rate
- end
-
- def test_vat_rate_defaults_to_effective_vat_rate_of_a_registrar
- registrar = registrars(:bestnames)
- invoice = @invoice.dup
- invoice.vat_rate = nil
- invoice.buyer = registrar
- invoice.items = @invoice.items
-
- registrar.stub(:effective_vat_rate, BigDecimal(55)) do
- invoice.save!
- end
-
- assert_equal BigDecimal(55), invoice.vat_rate
- end
-
- def test_vat_rate_cannot_be_updated
- @invoice.vat_rate = BigDecimal(21)
+ @invoice.vat_rate = BigDecimal('25.5')
@invoice.save!
@invoice.reload
- refute_equal BigDecimal(21), @invoice.vat_rate
+ assert_equal BigDecimal('25.5'), @invoice.vat_rate
end
def test_calculates_vat_amount
@@ -88,11 +46,6 @@ class InvoiceTest < ActiveSupport::TestCase
assert_equal 10, invoice.vat_amount
end
- def test_vat_amount_is_zero_when_vat_rate_is_blank
- @invoice.vat_rate = nil
- assert_equal 0, @invoice.vat_amount
- end
-
def test_calculates_subtotal
line_item = InvoiceItem.new
invoice = Invoice.new(items: [line_item, line_item])
diff --git a/test/models/registrar/vat_test.rb b/test/models/registrar/vat_test.rb
deleted file mode 100644
index aae017690..000000000
--- a/test/models/registrar/vat_test.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-require 'test_helper'
-
-class RegistrarVATTest < ActiveSupport::TestCase
- setup do
- @registrar = registrars(:bestnames)
- end
-
- def test_optional_vat_no
- @registrar.vat_no = ''
- assert @registrar.valid?
-
- @registrar.vat_no = 'any'
- assert @registrar.valid?
- end
-
- def test_apply_vat_rate_from_registry_when_registrar_is_local_vat_payer
- Setting.registry_country_code = 'US'
- @registrar.address_country_code = 'US'
-
- Registry.instance.stub(:vat_rate, BigDecimal('5.5')) do
- assert_equal BigDecimal('5.5'), @registrar.effective_vat_rate
- end
- end
-
- def test_require_no_vat_rate_when_registrar_is_local_vat_payer
- @registrar.vat_rate = 1
- assert @registrar.invalid?
-
- @registrar.vat_rate = nil
- assert @registrar.valid?
- end
-
- def test_apply_vat_rate_from_registrar_when_registrar_is_foreign_vat_payer
- Setting.registry_country_code = 'US'
- @registrar.address_country_code = 'DE'
- @registrar.vat_rate = BigDecimal('5.6')
- assert_equal BigDecimal('5.6'), @registrar.effective_vat_rate
- end
-
- def test_require_vat_rate_when_registrar_is_foreign_vat_payer_and_vat_no_is_absent
- @registrar.address_country_code = 'DE'
- @registrar.vat_no = ''
-
- @registrar.vat_rate = ''
- assert @registrar.invalid?
- assert @registrar.errors.added?(:vat_rate, :blank)
-
- @registrar.vat_rate = 5
- assert @registrar.valid?
- end
-
- def test_require_no_vat_rate_when_registrar_is_foreign_vat_payer_and_vat_no_is_present
- @registrar.address_country_code = 'DE'
- @registrar.vat_no = 'valid'
-
- @registrar.vat_rate = 1
- assert @registrar.invalid?
-
- @registrar.vat_rate = nil
- assert @registrar.valid?
- end
-
- def test_vat_rate_validation
- @registrar.address_country_code = 'DE'
- @registrar.vat_no = ''
-
- @registrar.vat_rate = -1
- assert @registrar.invalid?
-
- @registrar.vat_rate = 0
- assert @registrar.valid?
-
- @registrar.vat_rate = 99.9
- assert @registrar.valid?
-
- @registrar.vat_rate = 100
- assert @registrar.invalid?
- end
-
- def test_serializes_and_deserializes_vat_rate
- @registrar.address_country_code = 'DE'
- @registrar.vat_rate = BigDecimal('25.5')
- @registrar.save!
- @registrar.reload
- assert_equal BigDecimal('25.5'), @registrar.vat_rate
- end
-
- def test_parses_vat_rate_as_a_string
- @registrar.vat_rate = '25.5'
- assert_equal BigDecimal('25.5'), @registrar.vat_rate
- end
-
- def test_treats_empty_vat_rate_as_nil
- @registrar.vat_rate = ''
- assert_nil @registrar.vat_rate
- end
-end
diff --git a/test/models/registrar_test.rb b/test/models/registrar_test.rb
index 7e31f1649..9d1aa4b5a 100644
--- a/test/models/registrar_test.rb
+++ b/test/models/registrar_test.rb
@@ -111,9 +111,82 @@ class RegistrarTest < ActiveSupport::TestCase
assert_equal 'Main Street 1, NY, NY State, 1234', registrar.address
end
+ def test_invalid_with_vat_rate_when_registrar_is_vat_liable_locally
+ registrar = registrar_with_local_vat_liability
+
+ registrar.vat_rate = 1
+
+ assert registrar.invalid?
+ assert_includes registrar.errors.full_messages_for(:vat_rate),
+ 'VAT rate must be blank when a registrar is VAT-registered in the same' \
+ ' country as registry'
+ end
+
+ def test_invalid_with_vat_rate_when_registrar_is_vat_liable_in_foreign_country_and_vat_no_is_present
+ registrar = registrar_with_foreign_vat_liability
+
+ registrar.vat_no = 'valid'
+ registrar.vat_rate = 1
+
+ assert registrar.invalid?
+ end
+
+ def test_invalid_without_vat_rate_when_registrar_is_vat_liable_in_foreign_country_and_vat_no_is_absent
+ registrar = registrar_with_foreign_vat_liability
+
+ registrar.vat_no = ''
+ registrar.vat_rate = ''
+
+ assert registrar.invalid?
+ end
+
+ def test_vat_rate_validation
+ registrar = registrar_with_foreign_vat_liability
+
+ registrar.vat_rate = -1
+ assert registrar.invalid?
+
+ registrar.vat_rate = 0
+ assert registrar.valid?
+
+ registrar.vat_rate = 99.9
+ assert registrar.valid?
+
+ registrar.vat_rate = 100
+ assert registrar.invalid?
+ end
+
+ def test_serializes_and_deserializes_vat_rate
+ @registrar.address_country_code = 'DE'
+ @registrar.vat_rate = BigDecimal('25.5')
+ @registrar.save!
+ @registrar.reload
+ assert_equal BigDecimal('25.5'), @registrar.vat_rate
+ end
+
+ def test_aliases_vat_country_to_country
+ vat_country = Country.new(:us)
+ registrar = Registrar.new(vat_country: vat_country)
+ assert_equal vat_country, registrar.vat_country
+ end
+
private
def valid_registrar
registrars(:bestnames)
end
-end
+
+ def registrar_with_local_vat_liability
+ registrar = valid_registrar
+ registrar.vat_country = Country.new(:us)
+ Registry.current.vat_country = Country.new(:us)
+ registrar
+ end
+
+ def registrar_with_foreign_vat_liability
+ registrar = valid_registrar
+ registrar.vat_country = Country.new(:gb)
+ Registry.current.vat_country = Country.new(:us)
+ registrar
+ end
+end
\ No newline at end of file
diff --git a/test/models/registry_test.rb b/test/models/registry_test.rb
index 80c88c8bf..5ec10ec9e 100644
--- a/test/models/registry_test.rb
+++ b/test/models/registry_test.rb
@@ -1,20 +1,12 @@
require 'test_helper'
class RegistryTest < ActiveSupport::TestCase
- setup do
- @registry = Registry.send(:new)
+ def test_returns_current_registry
+ Setting.registry_vat_prc = 0.2
+ Setting.registry_country_code = 'US'
+
+ registry = Registry.current
+ assert_equal 20, registry.vat_rate
+ assert_equal Country.new(:us), registry.vat_country
end
-
- def test_implements_singleton
- assert_equal Registry.instance.object_id, Registry.instance.object_id
- end
-
- def test_vat_rate
- original_vat_prc = Setting.registry_vat_prc
- Setting.registry_vat_prc = 0.25
-
- assert_equal BigDecimal(25), @registry.vat_rate
-
- Setting.registry_vat_prc = original_vat_prc
- end
-end
+end
\ No newline at end of file
diff --git a/test/tasks/data_migrations/populate_invoice_vat_rate_test.rb b/test/tasks/data_migrations/populate_invoice_vat_rate_test.rb
new file mode 100644
index 000000000..e587117d8
--- /dev/null
+++ b/test/tasks/data_migrations/populate_invoice_vat_rate_test.rb
@@ -0,0 +1,40 @@
+require 'test_helper'
+
+class PopulateInvoiceVatRateTaskTest < ActiveSupport::TestCase
+ def test_populates_invoice_issue_date
+ invoice = invoice_without_vat_rate
+
+ capture_io do
+ run_task
+ end
+ invoice.reload
+
+ assert_not_nil invoice.vat_rate
+ end
+
+ def test_output
+ eliminate_effect_of_all_invoices_except(invoice_without_vat_rate)
+
+ assert_output "Invoices processed: 1\n" do
+ run_task
+ end
+ end
+
+ private
+
+ def invoice_without_vat_rate
+ invoice = invoices(:one)
+ invoice.update_columns(vat_rate: nil)
+ invoice
+ end
+
+ def eliminate_effect_of_all_invoices_except(invoice)
+ Invoice.connection.disable_referential_integrity do
+ Invoice.delete_all("id != #{invoice.id}")
+ end
+ end
+
+ def run_task
+ Rake::Task['data_migrations:populate_invoice_vat_rate'].execute
+ end
+end
\ No newline at end of file