Browse Source

CLDC-3930 CLDC-3931 CLDC-3932 CLDC-3933 CLDC-3934 add staircasing validations (#3001)

* validate staircasing dates

* validate staircasing percentage totals

* don't show both similar validations at once

* only validate against saledate when saledate present

* format numbers in error message

* add tests for staircasing validations

* linting

* don't block log creation on staircasing date errors

* combine similar validation methods

* update tests

* fix syntax

---------

Co-authored-by: Carolyn <carolyn.barker@softwire.com>
pull/3013/head^2
carolynbarker 3 weeks ago committed by GitHub
parent
commit
b05032bdc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      app/models/validations/sales/financial_validations.rb
  2. 22
      app/models/validations/sales/sale_information_validations.rb
  3. 8
      config/locales/validations/sales/financial.en.yml
  4. 9
      config/locales/validations/sales/sale_information.en.yml
  5. 52
      spec/models/validations/sales/financial_validations_spec.rb
  6. 157
      spec/models/validations/sales/sale_information_validations_spec.rb

24
app/models/validations/sales/financial_validations.rb

@ -105,16 +105,36 @@ module Validations::Sales::FinancialValidations
end
end
def validate_equity_less_than_staircase_difference(record)
def validate_staircase_difference(record)
return unless record.equity && record.stairbought && record.stairowned
return unless record.saledate && record.form.start_year_2024_or_later?
if record.equity > record.stairowned - record.stairbought
percentage_left = record.stairowned - record.stairbought - record.equity
if percentage_left.negative?
formatted_equity = sprintf("%g", record.equity)
joint_purchase_id = record.joint_purchase? ? "joint_purchase" : "not_joint_purchase"
record.errors.add :equity, I18n.t("validations.sales.financial.equity.equity_over_stairowned_minus_stairbought.#{joint_purchase_id}", equity: formatted_equity, staircase_difference: record.stairowned - record.stairbought)
record.errors.add :stairowned, I18n.t("validations.sales.financial.stairowned.equity_over_stairowned_minus_stairbought.#{joint_purchase_id}", equity: formatted_equity, staircase_difference: record.stairowned - record.stairbought)
record.errors.add :stairbought, I18n.t("validations.sales.financial.stairbought.equity_over_stairowned_minus_stairbought.#{joint_purchase_id}", equity: formatted_equity, staircase_difference: record.stairowned - record.stairbought)
elsif record.numstair
# We must use the lowest possible percentage for a staircasing transaction of any saletype, any year since 1980
minimum_percentage_per_staircasing_transaction = 1
previous_staircasing_transactions = record.numstair - 1
if percentage_left < previous_staircasing_transactions * minimum_percentage_per_staircasing_transaction
equity_sum = sprintf("%g", record.stairowned - percentage_left + previous_staircasing_transactions * minimum_percentage_per_staircasing_transaction)
formatted_equity = sprintf("%g", record.equity)
formatted_stairbought = sprintf("%g", record.stairbought)
formatted_stairowned = sprintf("%g", record.stairowned)
record.errors.add :equity, I18n.t("validations.sales.financial.equity.more_than_stairowned_minus_stairbought_minus_prev_staircasing", equity: formatted_equity, bought: formatted_stairbought, numprevstair: previous_staircasing_transactions, equity_sum:, stair_total: formatted_stairowned)
record.errors.add :stairowned, I18n.t("validations.sales.financial.stairowned.less_than_stairbought_plus_equity_plus_prev_staircasing", equity: formatted_equity, bought: formatted_stairbought, numprevstair: previous_staircasing_transactions, equity_sum:, stair_total: formatted_stairowned)
record.errors.add :stairbought, I18n.t("validations.sales.financial.stairbought.more_than_stairowned_minus_equity_minus_prev_staircasing", equity: formatted_equity, bought: formatted_stairbought, numprevstair: previous_staircasing_transactions, equity_sum:, stair_total: formatted_stairowned)
record.errors.add :numstair, I18n.t("validations.sales.financial.numstair.too_high_for_stairowned_minus_stairbought_minus_equity", equity: formatted_equity, bought: formatted_stairbought, numprevstair: previous_staircasing_transactions, equity_sum:, stair_total: formatted_stairowned)
end
end
end

22
app/models/validations/sales/sale_information_validations.rb

@ -41,6 +41,28 @@ module Validations::Sales::SaleInformationValidations
if record.initialpurchase < Time.zone.local(1980, 1, 1)
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980")
end
if record.saledate.present? && record.initialpurchase > record.saledate
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_saledate")
record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_initial_purchase_date")
end
end
def validate_staircasing_last_transaction_date(record)
return unless record.lasttransaction
if record.lasttransaction < Time.zone.local(1980, 1, 1)
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_1980")
end
if record.saledate.present? && record.lasttransaction > record.saledate
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_before_saledate")
record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_last_transaction_date")
end
if record.initialpurchase.present? && record.lasttransaction < record.initialpurchase
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_last_transaction")
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_initial_purchase")
end
end
def validate_previous_property_unit_type(record)

8
config/locales/validations/sales/financial.en.yml

@ -63,6 +63,7 @@ en:
equity_over_stairowned_minus_stairbought:
joint_purchase: "The initial equity stake is %{equity}% and the percentage owned in total minus the percentage bought is %{staircase_difference}%. In a staircasing transaction, the equity stake purchased cannot be larger than the percentage the buyers own minus the percentage bought."
not_joint_purchase: "The initial equity stake is %{equity}% and the percentage owned in total minus the percentage bought is %{staircase_difference}%. In a staircasing transaction, the equity stake purchased cannot be larger than the percentage the buyer owns minus the percentage bought."
more_than_stairowned_minus_stairbought_minus_prev_staircasing: "The initial equity stake is %{equity}%, the percentage bought is %{bought}%, and there have been %{numprevstair} previous staircasing transactions, totalling at least %{equity_sum}%, which is more than the total percentage owned by the buyers (%{stair_total}%). In a staircasing transaction, the total percentage owned must be at least the initial equity stake plus the percentage bought plus a minimum of 1% for each previous staircasing transaction."
stairowned:
equity_over_stairowned_minus_stairbought:
@ -72,6 +73,7 @@ en:
joint_purchase: "Total percentage buyers now own must be more than percentage bought in this transaction."
not_joint_purchase: "Total percentage buyer now owns must be more than percentage bought in this transaction."
percentage_bought_equal_percentage_owned: "The percentage bought is %{stairbought}% and the percentage owned in total is %{stairowned}%. These figures cannot be the same."
less_than_stairbought_plus_equity_plus_prev_staircasing: "The initial equity stake is %{equity}%, the percentage bought is %{bought}%, and there have been %{numprevstair} previous staircasing transactions, totalling at least %{equity_sum}%, which is more than the total percentage owned by the buyers (%{stair_total}%). In a staircasing transaction, the total percentage owned must be at least the initial equity stake plus the percentage bought plus a minimum of 1% for each previous staircasing transaction."
stairbought:
equity_over_stairowned_minus_stairbought:
@ -79,7 +81,11 @@ en:
not_joint_purchase: "The initial equity stake is %{equity}% and the percentage owned in total minus the percentage bought is %{staircase_difference}%. In a staircasing transaction, the equity stake purchased cannot be larger than the percentage the buyer owns minus the percentage bought."
percentage_bought_must_be_at_least_threshold: "The minimum percentage share that can be bought in a staircasing transaction for %{shared_ownership_type} is %{threshold}%. Please change your answer or check that you have selected the correct shared ownership type."
percentage_bought_equal_percentage_owned: "The percentage bought is %{stairbought}% and the percentage owned in total is %{stairowned}%. These figures cannot be the same."
more_than_stairowned_minus_equity_minus_prev_staircasing: "The initial equity stake is %{equity}%, the percentage bought is %{bought}%, and there have been %{numprevstair} previous staircasing transactions, totalling at least %{equity_sum}%, which is more than the total percentage owned by the buyers (%{stair_total}%). In a staircasing transaction, the total percentage owned must be at least the initial equity stake plus the percentage bought plus a minimum of 1% for each previous staircasing transaction."
numstair:
too_high_for_stairowned_minus_stairbought_minus_equity: "The initial equity stake is %{equity}%, the percentage bought is %{bought}%, and there have been %{numprevstair} previous staircasing transactions, totalling at least %{equity_sum}%, which is more than the total percentage owned by the buyers (%{stair_total}%). In a staircasing transaction, the total percentage owned must be at least the initial equity stake plus the percentage bought plus a minimum of 1% for each previous staircasing transaction."
uprn_selection:
outside_london_income_range: "Income must be between £0 and £90,000 for properties within a London local authority."
outside_non_london_income_range: "Income must be between £0 and £80,000 for properties in a non-London local authority."

9
config/locales/validations/sales/sale_information.en.yml

@ -16,11 +16,20 @@ en:
must_be_after_exdate: "Sale completion date must be after contract exchange date."
must_be_less_than_1_year_from_exdate: "Sale completion date must be less than 1 year after contract exchange date."
mortgage_used_year: "You must answer either ‘yes’ or ‘no’ to the question ‘was a mortgage used’ for the selected year."
must_be_after_initial_purchase_date: "Sale completion date for a staircasing transaction must be after the date of the initial purchase of a share."
must_be_after_last_transaction_date: "Sale completion date must be after the date of the last staircasing transaction."
exdate:
must_be_before_saledate: "Contract exchange date must be before sale completion date."
must_be_less_than_1_year_from_saledate: "Contract exchange date must be less than 1 year before sale completion date."
initialpurchase:
must_be_after_1980: "The initial purchase date must be after January 1, 1980."
must_be_before_last_transaction: "The initial purchase date must be before the last staircasing transaction date."
must_be_before_saledate: "The initial purchase date must be before the date of this sale."
lasttransaction:
must_be_after_1980: "The last staircasing transaction date must be after January 1, 1980."
must_be_after_initial_purchase: "The last staircasing transaction date must be after the initial purchase date."
must_be_before_saledate: "The last staircasing transaction date must be before the date of this sale."
fromprop:
previous_property_type_bedsit: "A bedsit cannot have more than 1 bedroom."
frombeds:

52
spec/models/validations/sales/financial_validations_spec.rb

@ -427,7 +427,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
end
end
describe "#validate_equity_less_than_staircase_difference" do
describe "#validate_staircase_difference" do
let(:record) { FactoryBot.build(:sales_log, saledate:) }
context "with a log in the 23/24 collection year" do
@ -437,7 +437,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairbought = 2
record.stairowned = 3
record.equity = 2
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
end
@ -450,7 +450,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairowned = 3
record.equity = 2
record.jointpur = 1
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors["equity"]).to include(I18n.t("validations.sales.financial.equity.equity_over_stairowned_minus_stairbought.joint_purchase", equity: 2, staircase_difference: 0.5))
expect(record.errors["stairowned"]).to include(I18n.t("validations.sales.financial.stairowned.equity_over_stairowned_minus_stairbought.joint_purchase", equity: 2, staircase_difference: 0.5))
expect(record.errors["stairbought"]).to include(I18n.t("validations.sales.financial.stairbought.equity_over_stairowned_minus_stairbought.joint_purchase", equity: 2, staircase_difference: 0.5))
@ -461,25 +461,25 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairowned = 3
record.equity = 2.5
record.jointpur = 2
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors["equity"]).to include(I18n.t("validations.sales.financial.equity.equity_over_stairowned_minus_stairbought.not_joint_purchase", equity: 2.5, staircase_difference: 1.0))
expect(record.errors["stairowned"]).to include(I18n.t("validations.sales.financial.stairowned.equity_over_stairowned_minus_stairbought.not_joint_purchase", equity: 2.5, staircase_difference: 1.0))
expect(record.errors["stairbought"]).to include(I18n.t("validations.sales.financial.stairbought.equity_over_stairowned_minus_stairbought.not_joint_purchase", equity: 2.5, staircase_difference: 1.0))
end
it "does not add errors if equity is less than stairowned - stairbought" do
it "does not add errors if equity is less than stairowned - stairbought and stairnum is nil" do
record.stairbought = 2
record.stairowned = 10
record.equity = 2
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
it "does not add errors if equity is equal stairowned - stairbought" do
it "does not add errors if equity is equal stairowned - stairbought and stairnum is nil" do
record.stairbought = 2
record.stairowned = 10
record.equity = 8
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
@ -487,7 +487,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairbought = nil
record.stairowned = 10
record.equity = 2
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
@ -495,7 +495,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairbought = 2
record.stairowned = nil
record.equity = 2
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
@ -503,7 +503,37 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairbought = 2
record.stairowned = 10
record.equity = 0
financial_validator.validate_equity_less_than_staircase_difference(record)
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
it "adds errors if stairnum is present and stairowned is not enough more than stairbought + equity" do
record.stairowned = 20
record.stairbought = 10
record.equity = 9
record.numstair = 3
financial_validator.validate_staircase_difference(record)
expect(record.errors["equity"]).to include(I18n.t("validations.sales.financial.equity.more_than_stairowned_minus_stairbought_minus_prev_staircasing", equity: 9, bought: 10, numprevstair: 2, equity_sum: 21, stair_total: 20))
expect(record.errors["stairowned"]).to include(I18n.t("validations.sales.financial.equity.more_than_stairowned_minus_stairbought_minus_prev_staircasing", equity: 9, bought: 10, numprevstair: 2, equity_sum: 21, stair_total: 20))
expect(record.errors["stairbought"]).to include(I18n.t("validations.sales.financial.equity.more_than_stairowned_minus_stairbought_minus_prev_staircasing", equity: 9, bought: 10, numprevstair: 2, equity_sum: 21, stair_total: 20))
expect(record.errors["numstair"]).to include(I18n.t("validations.sales.financial.equity.more_than_stairowned_minus_stairbought_minus_prev_staircasing", equity: 9, bought: 10, numprevstair: 2, equity_sum: 21, stair_total: 20))
end
it "does not add errors if stairnum is present and stairowned is enough more than stairbought + equity" do
record.stairowned = 25
record.stairbought = 10
record.equity = 9
record.numstair = 3
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
it "does not add errors if stairnum is present and stairowned exactly equals minimum" do
record.stairowned = 20
record.stairbought = 10
record.equity = 9
record.numstair = 2
financial_validator.validate_staircase_difference(record)
expect(record.errors).to be_empty
end
end

157
spec/models/validations/sales/sale_information_validations_spec.rb

@ -182,6 +182,163 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end
end
describe "#validate_staircasing_initial_purchase_date" do
context "when initial purchase date blank" do
let(:record) { build(:sales_log, initialpurchase: nil) }
it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).not_to be_present
end
end
context "when initial purchase date in 1979" do
let(:record) { build(:sales_log, initialpurchase: Date.new(1979, 12, 31)) }
it "adds an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).to be_present
end
end
context "when initial purchase date in 1980" do
let(:record) { build(:sales_log, initialpurchase: Date.new(1980, 1, 1)) }
it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).not_to be_present
end
end
context "when initial purchase date before saledate" do
let(:record) { build(:sales_log, initialpurchase: 2.months.ago, saledate: 1.month.ago) }
it "does not add the error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).not_to be_present
end
end
context "when initial purchase date after saledate" do
let(:record) { build(:sales_log, initialpurchase: 1.month.ago, saledate: 2.months.ago) }
it "adds error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).to eq([I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_saledate")])
expect(record.errors[:saledate]).to eq([I18n.t("validations.sales.sale_information.saledate.must_be_after_initial_purchase_date")])
end
end
context "when initial purchase date == saledate" do
let(:record) { build(:sales_log, initialpurchase: Time.zone.parse("2023-07-01"), saledate: Time.zone.parse("2023-07-01")) }
it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).not_to be_present
end
end
end
describe "#validate_staircasing_last_transaction_date" do
context "when last transaction date blank" do
let(:record) { build(:sales_log, lasttransaction: nil) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "when last transaction date in 1979" do
let(:record) { build(:sales_log, lasttransaction: Date.new(1979, 12, 31)) }
it "adds an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).to be_present
end
end
context "when last transaction date in 1980" do
let(:record) { build(:sales_log, lasttransaction: Date.new(1980, 1, 1)) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "when last transaction date before saledate" do
let(:record) { build(:sales_log, lasttransaction: 2.months.ago, saledate: 1.month.ago) }
it "does not add the error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "when last transaction date after saledate" do
let(:record) { build(:sales_log, lasttransaction: 1.month.ago, saledate: 2.months.ago) }
it "adds error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).to eq([I18n.t("validations.sales.sale_information.lasttransaction.must_be_before_saledate")])
expect(record.errors[:saledate]).to eq([I18n.t("validations.sales.sale_information.saledate.must_be_after_last_transaction_date")])
end
end
context "when last transaction date == saledate" do
let(:record) { build(:sales_log, lasttransaction: Time.zone.parse("2023-07-01"), saledate: Time.zone.parse("2023-07-01")) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "when last transaction date after initial purchase date" do
let(:record) { build(:sales_log, initialpurchase: 2.months.ago, lasttransaction: 1.month.ago) }
it "does not add the error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "when last transaction date before initial purchase date" do
let(:record) { build(:sales_log, initialpurchase: 1.month.ago, lasttransaction: 2.months.ago) }
it "adds error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).to eq([I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_initial_purchase")])
expect(record.errors[:initialpurchase]).to eq([I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_last_transaction")])
end
end
context "when last transaction date == initial purchase date" do
let(:record) { build(:sales_log, lasttransaction: Time.zone.parse("2023-07-01"), initialpurchase: Time.zone.parse("2023-07-01")) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
end
describe "#validate_previous_property_unit_type" do
context "when number of bedrooms is <= 1" do
let(:record) { FactoryBot.build(:sales_log, frombeds: 1, fromprop: 2) }

Loading…
Cancel
Save