diff --git a/app/models/validations/sales/financial_validations.rb b/app/models/validations/sales/financial_validations.rb index ae98184c9..72b80874b 100644 --- a/app/models/validations/sales/financial_validations.rb +++ b/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 diff --git a/app/models/validations/sales/sale_information_validations.rb b/app/models/validations/sales/sale_information_validations.rb index 3ddecaedb..f34eec790 100644 --- a/app/models/validations/sales/sale_information_validations.rb +++ b/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) diff --git a/config/locales/validations/sales/financial.en.yml b/config/locales/validations/sales/financial.en.yml index a4fe65464..50d8fe90a 100644 --- a/config/locales/validations/sales/financial.en.yml +++ b/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." diff --git a/config/locales/validations/sales/sale_information.en.yml b/config/locales/validations/sales/sale_information.en.yml index fecdb4a13..3c39608cc 100644 --- a/config/locales/validations/sales/sale_information.en.yml +++ b/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: diff --git a/spec/models/validations/sales/financial_validations_spec.rb b/spec/models/validations/sales/financial_validations_spec.rb index f49179063..3a2a7b7cd 100644 --- a/spec/models/validations/sales/financial_validations_spec.rb +++ b/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 diff --git a/spec/models/validations/sales/sale_information_validations_spec.rb b/spec/models/validations/sales/sale_information_validations_spec.rb index da49d1936..a43e0e639 100644 --- a/spec/models/validations/sales/sale_information_validations_spec.rb +++ b/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) }