diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 654808d3a..616ce64ae 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -268,6 +268,12 @@ class SalesLog < Log value * equity / 100 end + def expected_shared_ownership_deposit_value_range + return unless value && equity + + (value * ((equity - 0.1) / 100)..value * ((equity + 0.1) / 100)) + end + def stairbought_part_of_value return unless value && stairbought @@ -468,6 +474,15 @@ class SalesLog < Log value - discount_amount end + def value_with_discount_range + return if value.blank? + return (value..value) if discount.nil? + + discount_amount_lower_bound = value * (discount - 0.1) / 100 + discount_amount_upper_bound = value * (discount + 0.1) / 100 + (value - discount_amount_upper_bound..value - discount_amount_lower_bound) + end + def mortgage_deposit_and_grant_total return if deposit.blank? diff --git a/app/models/validations/sales/sale_information_validations.rb b/app/models/validations/sales/sale_information_validations.rb index 063c16599..399deb724 100644 --- a/app/models/validations/sales/sale_information_validations.rb +++ b/app/models/validations/sales/sale_information_validations.rb @@ -80,10 +80,7 @@ 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 - - if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil?) && record.discounted_ownership_sale? + if mortgage_deposit_and_grant_total_is_over_discount_tolerance?(record) && 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| @@ -207,7 +204,7 @@ module Validations::Sales::SaleInformationValidations 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 total_is_over_expected_shared_ownership_deposit_tolerance?(record.mortgage_deposit_and_discount_total, record) %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 +225,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 total_is_over_expected_shared_ownership_deposit_tolerance?(record.deposit_and_discount_total, record) %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"), @@ -253,7 +250,7 @@ module Validations::Sales::SaleInformationValidations if record.mortgage_used? return unless record.mortgage - if over_tolerance?(record.mortgage_and_deposit_total, record.expected_shared_ownership_deposit_value, 1) + if total_is_over_expected_shared_ownership_deposit_tolerance?(record.mortgage_and_deposit_total, record) %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 +269,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 total_is_over_expected_shared_ownership_deposit_tolerance?(record.deposit, record) %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"), @@ -396,6 +393,25 @@ module Validations::Sales::SaleInformationValidations end end + def total_is_over_expected_shared_ownership_deposit_tolerance?(total, record) + if record.form.start_year_2026_or_later? + !record.expected_shared_ownership_deposit_value_range.cover?(total) + else + over_tolerance?(total, record.expected_shared_ownership_deposit_value, 1) + end + end + + def mortgage_deposit_and_grant_total_is_over_discount_tolerance?(record) + if record.form.start_year_2026_or_later? + !record.value_with_discount_range.cover?(record.mortgage_deposit_and_grant_total) + else + # we found that this tolerance still wasn't working for us, so for post 2026 we just calculate a range for 0.1% higher and lower. + # this means we can be certain an answer 0.1% higher or lower will always be accepted without needed to backsolve for the tolerance + tolerance = record.discount ? record.value * 0.05 / 100 : 1 + over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: record.discount.present?) + end + end + def over_tolerance?(expected, actual, tolerance, strict: false) if strict (expected - actual).abs > tolerance