Browse Source

CLDC-4212, CLDC-4213, CLDC-4214: Add 0.1% tolerance to all calculations that involve %s of a value (#3214)

* CLDC-4212: Add 0.1% tolerance to % calculations

this is to specifically allow 0.1% more or less than the calculation expects, for those that contain %s in them

this removes difficulties on adding 66.6% equity (or should it be 66.7%?)

* CLDC-4212: Remove discount soft validation

was replaced by a hard validation in CLDC-1899 so no longer serves a purpose

* CLDC-4212: Add tests

* fixup! CLDC-4212: Add 0.1% tolerance to % calculations

calculate a tolerance instead

* fixup! CLDC-4212: Add 0.1% tolerance to % calculations

update comment
pull/3228/head
Samuel Young 6 days ago committed by GitHub
parent
commit
d2927f3e7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 32
      app/models/sales_log.rb
  2. 17
      app/models/validations/sales/sale_information_validations.rb
  3. 1
      app/models/validations/sales/soft_validations.rb
  4. 116
      spec/models/validations/sales/sale_information_validations_spec.rb

32
app/models/sales_log.rb

@ -268,6 +268,22 @@ class SalesLog < Log
value * equity / 100
end
def expected_shared_ownership_deposit_value_tolerance
return 1 unless value && equity
# we found that a fixed tolerance was not quite what we wanted here.
# CORE wants it so if a user say, has a 66.666% equity they can enter either 66.6% or 66.7% (or 66.5%)
# so in 2026 we base our tolerance off of a discount 0.1% higher or lower
if form.start_year_2026_or_later?
lower_bound = value * ((equity - 0.1) / 100)
upper_bound = value * ((equity + 0.1) / 100)
(upper_bound - lower_bound) / 2
else
1
end
end
def stairbought_part_of_value
return unless value && stairbought
@ -468,6 +484,22 @@ class SalesLog < Log
value - discount_amount
end
def value_with_discount_tolerance
return 1 if value.blank? || discount.nil?
# we found that a simple tolerance was not quite what we wanted here.
# CORE wants it so if a user say, has a 66.6% discount then can enter either 66.6% or 66.7%
# so in 2026 we base our tolerance off of a discount 0.1% higher or lower
if form.start_year_2026_or_later?
discount_amount_lower_bound = value * (discount - 0.1) / 100
discount_amount_upper_bound = value * (discount + 0.1) / 100
(discount_amount_upper_bound - discount_amount_lower_bound) / 2
else
discount ? value * 0.05 / 100 : 1
end
end
def mortgage_deposit_and_grant_total
return if deposit.blank?

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

@ -80,10 +80,9 @@ module Validations::Sales::SaleInformationValidations
return unless record.mortgage || record.mortgageused == 2 || record.mortgageused == 3
return unless record.discount || record.grant || record.type == 29
# When a percentage discount is used, a percentage tolerance is needed to account for rounding errors
tolerance = record.discount ? record.value * 0.05 / 100 : 1
tolerance = record.value_with_discount_tolerance
if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil?) && record.discounted_ownership_sale?
if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil? || record.form.start_year_2026_or_later?) && record.discounted_ownership_sale?
deposit_and_grant_sentence = record.grant.present? ? ", cash deposit (#{record.field_formatted_as_currency('deposit')}), and grant (#{record.field_formatted_as_currency('grant')})" : " and cash deposit (#{record.field_formatted_as_currency('deposit')})"
discount_sentence = record.discount.present? ? " (#{record.field_formatted_as_currency('value')}) subtracted by the sum of the full purchase price (#{record.field_formatted_as_currency('value')}) multiplied by the percentage discount (#{record.discount}%)" : ""
%i[mortgageused mortgage value deposit discount grant].each do |field|
@ -204,10 +203,12 @@ module Validations::Sales::SaleInformationValidations
def check_non_staircasing_socialhomebuy_mortgage(record)
return unless record.cashdis
tolerance = record.expected_shared_ownership_deposit_value_tolerance
if record.mortgage_used?
return unless record.mortgage
if over_tolerance?(record.mortgage_deposit_and_discount_total, record.expected_shared_ownership_deposit_value, 1)
if over_tolerance?(record.mortgage_deposit_and_discount_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgage value deposit cashdis equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used_socialhomebuy",
mortgage: record.field_formatted_as_currency("mortgage"),
@ -228,7 +229,7 @@ module Validations::Sales::SaleInformationValidations
expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe
end
elsif record.mortgage_not_used?
if over_tolerance?(record.deposit_and_discount_total, record.expected_shared_ownership_deposit_value, 1)
if over_tolerance?(record.deposit_and_discount_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgageused value deposit cashdis equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used_socialhomebuy",
deposit_and_discount_total: record.field_formatted_as_currency("deposit_and_discount_total"),
@ -250,10 +251,12 @@ module Validations::Sales::SaleInformationValidations
end
def check_non_staircasing_non_socialhomebuy_mortgage(record)
tolerance = record.expected_shared_ownership_deposit_value_tolerance
if record.mortgage_used?
return unless record.mortgage
if over_tolerance?(record.mortgage_and_deposit_total, record.expected_shared_ownership_deposit_value, 1)
if over_tolerance?(record.mortgage_and_deposit_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgage value deposit equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used",
mortgage: record.field_formatted_as_currency("mortgage"),
@ -272,7 +275,7 @@ module Validations::Sales::SaleInformationValidations
expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe
end
elsif record.mortgage_not_used?
if over_tolerance?(record.deposit, record.expected_shared_ownership_deposit_value, 1)
if over_tolerance?(record.deposit, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgageused value deposit equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used",
deposit: record.field_formatted_as_currency("deposit"),

1
app/models/validations/sales/soft_validations.rb

@ -117,6 +117,7 @@ module Validations::Sales::SoftValidations
def mortgage_plus_deposit_less_than_discounted_value?
return unless mortgage && deposit && value && discount
return if form.start_year_2026_or_later?
discounted_value = value * (100 - discount) / 100
mortgage + deposit < discounted_value

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

@ -663,6 +663,46 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
expect(record.errors["grant"]).to be_empty
end
end
context "with year 2026", :aggregate_failures do
let(:saledate) { Time.zone.local(2026, 4, 1) }
context "when mortgage and deposit is exact" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10, ownershipsch: 2, type: 9) }
it "does not add an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["discount"]).to be_empty
end
end
context "when mortgage and deposit is within 0.1% discount tolerance" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10.1, ownershipsch: 2, type: 9) }
it "does not add an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["discount"]).to be_empty
end
end
context "when mortgage and deposit is outside 0.1% discount tolerance" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10.2, ownershipsch: 2, type: 9) }
it "adds an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["discount"]).not_to be_empty
end
end
end
end
describe "#validate_outright_sale_value_matches_mortgage_plus_deposit" do
@ -1171,6 +1211,82 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end
end
end
context "with year 2026", :aggregate_failures do
let(:saledate) { Time.zone.local(2026, 4, 1) }
context "when mortgage and deposit is exact" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when mortgage and deposit is within 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15.1, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when mortgage and deposit is outside 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15.2, ownershipsch: 1, type: 30, saledate:) }
it "adds an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["equity"]).not_to be_empty
end
end
context "when deposit (no mortgage) is exact" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when deposit (no mortgage) is within 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15.1, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when deposit (no mortgage) is outside 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15.2, ownershipsch: 1, type: 30, saledate:) }
it "adds an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["equity"]).not_to be_empty
end
end
end
end
describe "#validate_staircasing_mortgage" do

Loading…
Cancel
Save