Browse Source

Merge branch 'main' into CLDC-4071-show-telephone-extensions-and-export

CLDC-4071-show-telephone-extensions-and-export
Nat Dean-Lewis 6 days ago
parent
commit
fbcad0e5b0
  1. 6
      app/controllers/auth/confirmations_controller.rb
  2. 1
      app/controllers/auth/passwords_controller.rb
  3. 2
      app/models/derived_variables/lettings_log_variables.rb
  4. 25
      app/models/form/lettings/pages/net_income_value_check.rb
  5. 9
      app/models/form/lettings/pages/person_known.rb
  6. 2
      app/models/form/lettings/questions/hhmemb.rb
  7. 10
      app/models/form/lettings/subsections/household_characteristics.rb
  8. 2
      app/models/form/lettings/subsections/income_and_benefits.rb
  9. 1
      app/models/form/sales/questions/building_height_class.rb
  10. 1
      app/models/form/sales/questions/management_fee.rb
  11. 1
      app/models/form/sales/questions/monthly_rent_before_staircasing.rb
  12. 1
      app/models/form/sales/questions/mortgage_amount.rb
  13. 1
      app/models/form/sales/questions/purchase_price.rb
  14. 1
      app/models/form/sales/questions/value.rb
  15. 3
      app/models/lettings_log.rb
  16. 8
      app/models/log.rb
  17. 1
      app/models/sales_log.rb
  18. 2
      app/models/user.rb
  19. 4
      app/models/validations/financial_validations.rb
  20. 6
      app/models/validations/sales/sale_information_validations.rb
  21. 35
      app/models/validations/soft_validations.rb
  22. 11
      app/services/bulk_upload/lettings/year2025/row_parser.rb
  23. 11
      app/services/bulk_upload/lettings/year2026/row_parser.rb
  24. 30
      app/services/bulk_upload/sales/year2025/row_parser.rb
  25. 29
      app/services/bulk_upload/sales/year2026/row_parser.rb
  26. 5
      app/views/form/guidance/_building_height_class.html.erb
  27. 2
      app/views/users/new.html.erb
  28. 2
      config/locales/forms/2024/sales/sale_information.en.yml
  29. 2
      config/locales/forms/2025/lettings/household_characteristics.en.yml
  30. 2
      config/locales/forms/2025/lettings/household_situation.en.yml
  31. 2
      config/locales/forms/2025/sales/sale_information.en.yml
  32. 2
      config/locales/forms/2026/lettings/household_characteristics.en.yml
  33. 2
      config/locales/forms/2026/lettings/household_situation.en.yml
  34. 9
      config/locales/forms/2026/sales/guidance.en.yml
  35. 4
      config/locales/forms/2026/sales/property_information.en.yml
  36. 2
      config/locales/forms/2026/sales/sale_information.en.yml
  37. 5
      db/migrate/20260420151627_add_force_reset_password_on_confirmation_to_users.rb
  38. 3
      db/schema.rb
  39. 16
      lib/tasks/correct_values_missing_max_for_2025_or_later_sales_logs.rake
  40. 13
      lib/tasks/fix_sales_logs_with_invalid_initialpurchase_lasttransaction.rake
  41. 6
      spec/fixtures/files/lettings_log_csv_export_codes_24.csv
  42. 6
      spec/fixtures/files/lettings_log_csv_export_codes_25.csv
  43. 6
      spec/fixtures/files/lettings_log_csv_export_codes_26.csv
  44. 6
      spec/fixtures/files/lettings_log_csv_export_labels_25.csv
  45. 6
      spec/fixtures/files/lettings_log_csv_export_labels_26.csv
  46. 4
      spec/models/form/lettings/pages/net_income_value_check_spec.rb
  47. 29
      spec/models/form/lettings/pages/person_known_spec.rb
  48. 48
      spec/models/form/lettings/subsections/household_characteristics_spec.rb
  49. 4
      spec/models/form/lettings/subsections/income_and_benefits_spec.rb
  50. 4
      spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb
  51. 4
      spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb
  52. 2
      spec/models/form/sales/pages/mortgage_amount_spec.rb
  53. 2
      spec/models/form/sales/pages/purchase_price_outright_ownership_spec.rb
  54. 2
      spec/models/form/sales/pages/purchase_price_spec.rb
  55. 2
      spec/models/form/sales/pages/value_shared_ownership_spec.rb
  56. 4
      spec/models/form/sales/questions/building_height_class_spec.rb
  57. 9
      spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb
  58. 6
      spec/models/form/sales/questions/mortgage_amount_spec.rb
  59. 10
      spec/models/form/sales/questions/purchase_price_spec.rb
  60. 14
      spec/models/form/sales/questions/value_spec.rb
  61. 12
      spec/models/validations/household_validations_spec.rb
  62. 69
      spec/models/validations/sales/sale_information_validations_spec.rb
  63. 17
      spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb
  64. 17
      spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb
  65. 28
      spec/services/bulk_upload/sales/year2025/row_parser_spec.rb
  66. 28
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

6
app/controllers/auth/confirmations_controller.rb

@ -5,7 +5,11 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
yield resource if block_given?
if resource.errors.empty?
if resource.sign_in_count.zero?
# previously we reset sign_in_count on deactivation and had only the .zero? check here.
# this would force a password reset both if it was your very first log in, and on your first login after reactivation.
# now we have a specific flag for the latter case as resetting sign_in_count was difficult for auditing.
# note that some deactivated users will have a sign_in_count of 0 and not have this flag set if they were deactivated before we made this change.
if resource.force_reset_password_on_confirmation || resource.sign_in_count.zero?
token = resource.send(:set_reset_password_token)
redirect_to "#{edit_user_password_url}?reset_password_token=#{token}&confirmation=true"
else

1
app/controllers/auth/passwords_controller.rb

@ -37,6 +37,7 @@ class Auth::PasswordsController < Devise::PasswordsController
if resource.errors.empty?
resource.unlock_access! if resource.respond_to?(:unlock_access!)
resource.force_reset_password_on_confirmation = false
if Devise.sign_in_after_reset_password
set_flash_message!(:notice, password_update_flash_message)
resource.after_database_authentication

2
app/models/derived_variables/lettings_log_variables.rb

@ -339,7 +339,7 @@ private
def infer_only_partner!(partner_number)
return unless hhmemb
(2..hhmemb).each do |i|
(2..people_with_details).each do |i|
next if i == partner_number
if ["P", nil].include?(public_send("relat#{i}"))

25
app/models/form/lettings/pages/net_income_value_check.rb

@ -1,9 +1,9 @@
class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "net_income_value_check"
def initialize(id, hsh, subsection, person_index: nil)
super(id, hsh, subsection)
@copy_key = "lettings.soft_validations.net_income_value_check"
@depends_on = [{ "net_income_soft_validation_triggered?" => true }]
@person_index = person_index
@depends_on = depends_on
@title_text = {
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text",
"arguments" => [
@ -32,6 +32,23 @@ class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
}
end
def depends_on
if @person_index.present?
[
{
"net_income_soft_validation_triggered?" => true,
"details_known_#{@person_index}" => 0,
},
]
else
[
{
"net_income_soft_validation_triggered?" => true,
},
]
end
end
def questions
@questions ||= [Form::Lettings::Questions::NetIncomeValueCheck.new(nil, nil, self)]
end

9
app/models/form/lettings/pages/person_known.rb

@ -2,11 +2,18 @@ class Form::Lettings::Pages::PersonKnown < ::Form::Page
def initialize(id, hsh, subsection, person_index:)
super(id, hsh, subsection)
@id = "person_#{person_index}_known"
@depends_on = (person_index..8).map { |index| { "hhmemb" => index } }
@person_index = person_index
@depends_on = depends_on
end
def questions
@questions ||= [Form::Lettings::Questions::DetailsKnown.new(nil, nil, self, person_index: @person_index)]
end
def depends_on
[{ "hhmemb" => {
"operator" => ">=",
"operand" => @person_index,
} }]
end
end

2
app/models/form/lettings/questions/hhmemb.rb

@ -5,7 +5,7 @@ class Form::Lettings::Questions::Hhmemb < ::Form::Question
@type = "numeric"
@width = 2
@check_answers_card_number = 0
@max = 8
@max = 15
@min = 1
@step = 1
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR)

10
app/models/form/lettings/subsections/household_characteristics.rb

@ -13,6 +13,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadHhmembValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdLeadHhmembValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_hhmemb_check", nil, self) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("hhmemb_net_income_value_check", nil, self),
Form::Lettings::Pages::LeadTenantAge.new(nil, nil, self),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
@ -37,6 +38,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::LeadTenantUnderRetirementValueCheck.new("working_situation_lead_tenant_under_retirement_value_check", nil, self),
Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new("working_situation_lead_tenant_over_retirement_value_check", nil, self),
(Form::Lettings::Pages::WorkingSituationIllnessCheckLead.new("working_situation_lead_tenant_long_term_illness_check", nil, self) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("working_situation_lead_tenant_net_income_value_check", nil, self),
*person_questions(person_index: 2),
*person_questions(person_index: 3),
*person_questions(person_index: 4),
@ -50,10 +52,14 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
def person_questions(person_index:)
[
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index:),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::NetIncomeValueCheck.new("age_#{person_index}_net_income_value_check", nil, self, person_index:) if form.start_year_2026_or_later?),
relationship_question(person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("relationship_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::MultiplePartnersValueCheck.new("relationship_#{person_index}_multiple_partners_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
@ -61,6 +67,8 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("age_#{person_index}_under_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("age_#{person_index}_over_retirement_value_check", nil, self, person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("age_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NetIncomeValueCheck.new("age_#{person_index}_net_income_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonSexRegisteredAtBirth.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderSameAsSex.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderIdentity.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
@ -68,10 +76,12 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_person_#{person_index}_check", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("gender_#{person_index}_over_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonWorkingSituation.new(nil, nil, self, person_index:),
Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("working_situation_#{person_index}_under_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("working_situation_#{person_index}_over_retirement_value_check", nil, self, person_index:),
(Form::Lettings::Pages::WorkingSituationIllnessCheckPerson.new("working_situation_#{person_index}_long_term_illness_check", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("working_situation_#{person_index}_net_income_value_check", nil, self, person_index:),
]
end

2
app/models/form/lettings/subsections/income_and_benefits.rb

@ -10,7 +10,7 @@ class Form::Lettings::Subsections::IncomeAndBenefits < ::Form::Subsection
@pages ||= [
Form::Lettings::Pages::IncomeKnown.new(nil, nil, self),
Form::Lettings::Pages::IncomeAmount.new(nil, nil, self),
Form::Lettings::Pages::NetIncomeValueCheck.new(nil, nil, self),
Form::Lettings::Pages::NetIncomeValueCheck.new("income_amount_net_income_value_check", nil, self),
Form::Lettings::Pages::HousingBenefit.new("housing_benefit", nil, self),
Form::Lettings::Pages::BenefitsProportion.new("benefits_proportion", nil, self),
Form::Lettings::Pages::RentOrOtherCharges.new(nil, nil, self),

1
app/models/form/sales/questions/building_height_class.rb

@ -3,6 +3,7 @@ class Form::Sales::Questions::BuildingHeightClass < ::Form::Question
super
@id = "buildheightclass"
@type = "radio"
@top_guidance_partial = "building_height_class"
@answer_options = ANSWER_OPTIONS
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR)
end

1
app/models/form/sales/questions/management_fee.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::ManagementFee < ::Form::Question
@copy_key = "sales.sale_information.management_fee.management_fee"
@type = "numeric"
@min = 1
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/monthly_rent_before_staircasing.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::MonthlyRentBeforeStaircasing < ::Form::Question
@copy_key = "sales.sale_information.mrent_staircasing.prestaircasing"
@type = "numeric"
@min = 0
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/mortgage_amount.rb

@ -4,6 +4,7 @@ class Form::Sales::Questions::MortgageAmount < ::Form::Question
@id = "mortgage"
@type = "numeric"
@min = 1
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = 1
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/purchase_price.rb

@ -4,6 +4,7 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
@id = "value"
@type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = form.start_year_2026_or_later? ? 1 : 0.01 # 0.01 was a mistake that was fixed in 2026
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/value.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::Value < ::Form::Question
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = 1
@width = 10
@prefix = "£"

3
app/models/lettings_log.rb

@ -191,6 +191,7 @@ class LettingsLog < Log
NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 11 => 51, 1 => 52, 10 => 53 }.freeze
SUFFIX_FROM_PERIOD = { 2 => "every 2 weeks", 3 => "every 4 weeks", 4 => "every month" }.freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id tenancycode startdate age1_known age1 sex1 sexrab1 ecstat1 tcharge household_charge chcharge].freeze
MAX_PEOPLE_WITH_DETAILS = 8 # This is not yet used in all lettings validations etc. so check for other occurrences of this concept if updating this
RENT_TYPE = {
social_rent: 0,
affordable_rent: 1,
@ -284,7 +285,7 @@ class LettingsLog < Log
range = ALLOWED_INCOME_RANGES[ecstat1].clone
if hhmemb > 1
(2..hhmemb).each do |person_index|
(2..people_with_details).each do |person_index|
ecstat = self["ecstat#{person_index}"]
if ecstat.nil?

8
app/models/log.rb

@ -204,6 +204,14 @@ class Log < ApplicationRecord
false
end
def people_with_details
[hhmemb || max_people_with_details, max_people_with_details].min
end
def max_people_with_details
self.class::MAX_PEOPLE_WITH_DETAILS
end
def ethnic_refused?
ethnic_group == 17
end

1
app/models/sales_log.rb

@ -104,6 +104,7 @@ class SalesLog < Log
OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 sexrab1 ecstat1 postcode_full uprn address_line1].freeze
MAX_PEOPLE_WITH_DETAILS = 6 # This is not yet used in all sales validations etc. so check for other occurrences of this concept if updating this
def lettings?
false

2
app/models/user.rb

@ -179,7 +179,7 @@ class User < ApplicationRecord
update!(
active: false,
confirmed_at: nil,
sign_in_count: 0,
force_reset_password_on_confirmation: true,
initial_confirmation_sent: false,
reactivate_with_organisation:,
unconfirmed_email: nil,

4
app/models/validations/financial_validations.rb

@ -41,7 +41,7 @@ module Validations::FinancialValidations
:over_hard_max,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_over_hard_max", earnings: format_as_currency(record.earnings), frequency:),
)
(1..record.hhmemb).each do |n|
(1..record.people_with_details).each do |n|
record.errors.add(
"ecstat#{n}",
:over_hard_max,
@ -70,7 +70,7 @@ module Validations::FinancialValidations
:under_hard_min,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_under_hard_min", earnings: format_as_currency(record.earnings), frequency:),
)
(1..record.hhmemb).each do |n|
(1..record.people_with_details).each do |n|
record.errors.add(
"ecstat#{n}",
:under_hard_min,

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

@ -42,7 +42,7 @@ module Validations::Sales::SaleInformationValidations
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980")
end
if record.saledate.present? && record.initialpurchase > record.saledate
if record.saledate.present? && ((record.initialpurchase > record.saledate) || (record.initialpurchase == record.saledate && record.form.start_year_2026_or_later?))
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
@ -55,11 +55,11 @@ module Validations::Sales::SaleInformationValidations
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_1980")
end
if record.saledate.present? && record.lasttransaction > record.saledate
if record.saledate.present? && ((record.lasttransaction > record.saledate) || (record.lasttransaction == record.saledate && record.form.start_year_2026_or_later?))
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
if record.initialpurchase.present? && ((record.lasttransaction < record.initialpurchase) || (record.lasttransaction == record.initialpurchase && record.form.start_year_2026_or_later?))
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

35
app/models/validations/soft_validations.rb

@ -208,8 +208,7 @@ module Validations::SoftValidations
def multiple_partners?
return unless hhmemb
max_person_with_details = sales? ? [hhmemb, 6].min : [hhmemb, 8].min
(2..max_person_with_details).many? { |n| public_send("relat#{n}") == "P" }
(2..people_with_details).many? { |n| public_send("relat#{n}") == "P" }
end
def at_least_one_working_situation_is_sickness_and_household_sickness_is_no?
@ -219,22 +218,18 @@ module Validations::SoftValidations
private
def all_tenants_age_and_gender_information_completed?
return false if hhmemb.present? && hhmemb > 8
return false if hhmemb.present? && hhmemb > max_people_with_details
return false unless all_tenants_gender_information_completed?
person_count = hhmemb || 8
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
public_send("age#{n}").present? && public_send("age#{n}_known").present? && public_send("age#{n}_known").zero?
end
end
def all_tenants_gender_information_completed?
return false if hhmemb.present? && hhmemb > 8
person_count = hhmemb || 8
return false if hhmemb.present? && hhmemb > max_people_with_details
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
tenant_gender_information_completed?(n)
end
end
@ -258,27 +253,21 @@ private
end
def any_non_male_in_expected_pregnancy_age_range(min, max)
person_count = hhmemb || 8
(1..person_count).any? do |n|
(1..people_with_details).any? do |n|
person_in_expected_pregnancy_age_range(n, min, max) && person_is_non_male(n)
end
end
def non_males_in_the_household?
person_count = hhmemb || 8
(1..person_count).any? do |n|
(1..people_with_details).any? do |n|
person_is_non_male(n)
end
end
def all_male_tenants_in_the_household?
return false if hhmemb.present? && hhmemb > 8
return false if hhmemb.present? && hhmemb > max_people_with_details
person_count = hhmemb || 8
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
person_is_male(n)
end
end
@ -344,11 +333,7 @@ private
end
def at_least_one_person_working_situation_is_illness?
return if hhmemb.present? && hhmemb > 8
person_count = hhmemb || 8
(1..person_count).any? { |n| public_send("ecstat#{n}") == 8 }
(1..people_with_details).any? { |n| public_send("ecstat#{n}") == 8 }
end
def no_one_in_household_with_illness?

11
app/services/bulk_upload/lettings/year2025/row_parser.rb

@ -461,7 +461,6 @@ class BulkUpload::Lettings::Year2025::RowParser
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? }
validate :validate_address_fields, on: :after_log, unless: -> { supported_housing? }
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log
@ -506,6 +505,8 @@ class BulkUpload::Lettings::Year2025::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
@valid = errors.blank?
@ -1035,6 +1036,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end

11
app/services/bulk_upload/lettings/year2026/row_parser.rb

@ -496,7 +496,6 @@ class BulkUpload::Lettings::Year2026::RowParser
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log
@ -541,6 +540,8 @@ class BulkUpload::Lettings::Year2026::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
@valid = errors.blank?
@ -1113,6 +1114,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end

30
app/services/bulk_upload/sales/year2025/row_parser.rb

@ -154,6 +154,7 @@ class BulkUpload::Sales::Year2025::RowParser
:field_52, # Gender identity of person 5
:field_56, # Gender identity of person 6
:field_58, # What was buyer 1’s previous tenure?
:field_64, # What was buyer 2’s previous tenure?
:field_75, # What is the total amount the buyers had in savings before they paid any deposit for the property?
@ -227,7 +228,7 @@ class BulkUpload::Sales::Year2025::RowParser
attribute :field_56, :string
attribute :field_57, :integer
attribute :field_58, :integer
attribute :field_58, :string
attribute :field_59, :integer
attribute :field_60, :string
attribute :field_61, :string
@ -438,8 +439,6 @@ class BulkUpload::Sales::Year2025::RowParser
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? }
@ -503,6 +502,8 @@ class BulkUpload::Sales::Year2025::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
errors.blank?
@ -564,6 +565,15 @@ private
end
end
def prevten
case field_58
when "R"
0
else
field_58
end
end
def prevtenbuy2
case field_64
when "R"
@ -689,6 +699,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end
@ -902,7 +920,7 @@ private
attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/)
attributes["prevown"] = field_76
attributes["prevten"] = field_58
attributes["prevten"] = prevten
attributes["prevloc"] = field_62
attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known
@ -1283,7 +1301,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership?
if [1, 2].include?(field_58) || [1, 2].include?(field_64.to_i)
if [1, 2].include?(field_58.to_i) || [1, 2].include?(field_64.to_i)
1
else
2
@ -1501,7 +1519,7 @@ private
next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) }
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end

29
app/services/bulk_upload/sales/year2026/row_parser.rb

@ -169,6 +169,7 @@ class BulkUpload::Sales::Year2026::RowParser
:field_61, # Person 5's sex, as registered at birth
:field_67, # Person 6's sex, as registered at birth
:field_71, # What was buyer 1’s previous tenure?
:field_77, # What was buyer 2’s previous tenure?
:field_88, # What is the total amount the buyers had in savings before they paid any deposit for the property?
@ -263,7 +264,7 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_69, :string
attribute :field_70, :integer
attribute :field_71, :integer
attribute :field_71, :string
attribute :field_72, :integer
attribute :field_73, :string
attribute :field_74, :string
@ -492,7 +493,6 @@ class BulkUpload::Sales::Year2026::RowParser
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
@ -560,6 +560,8 @@ class BulkUpload::Sales::Year2026::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
errors.blank?
@ -621,6 +623,15 @@ private
end
end
def prevten
case field_71
when "R"
0
else
field_71
end
end
def prevtenbuy2
case field_77
when "R"
@ -750,6 +761,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end
@ -995,7 +1014,7 @@ private
attributes["savings"] = field_88.to_i if attributes["savingsnk"]&.zero? && field_88&.match(/\A\d+\z/)
attributes["prevown"] = field_89
attributes["prevten"] = field_71
attributes["prevten"] = prevten
attributes["prevloc"] = field_75
attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known
@ -1416,7 +1435,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership?
if [1, 2].include?(field_71) || [1, 2].include?(field_77.to_i)
if [1, 2].include?(field_71.to_i) || [1, 2].include?(field_77.to_i)
1
else
2
@ -1654,7 +1673,7 @@ private
next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) }
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end

5
app/views/form/guidance/_building_height_class.html.erb

@ -0,0 +1,5 @@
<div class="govuk-body">
<%= govuk_details(summary_text: I18n.t("forms.#{@log.form.start_date.year}.sales.guidance.building_height_class.title")) do %>
<%= I18n.t("forms.#{@log.form.start_date.year}.sales.guidance.building_height_class.content").html_safe %>
<% end %>
</div>

2
app/views/users/new.html.erb

@ -54,7 +54,7 @@
options: { disabled: [""], selected: @organisation_id ? answer_options.first : "" } %>
<% end %>
<% hints_for_roles = { data_provider: ["Can view and submit logs for this organisation"], data_coordinator: ["Can view and submit logs for this organisation and any of its managing agents", "Can manage details for this organisation", "Can manage users for this organisation"], support: nil } %>
<% hints_for_roles = { data_provider: ["Can view and submit logs for this organisation"], data_coordinator: ["Can view and submit logs for this organisation and any of its managing agents", "Can manage details for this organisation", "Can manage users for this organisation"], support: ["Can only be created for the MHCLG organisation in the CORE service, to be used by MHCLG and its contractor staff", "Has access to all organisations' data across the CORE service", "Cannot be created for users in housing organisations as this would be a data protection breach"] } %>
<% roles_with_hints = current_user.assignable_roles.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: hints_for_roles[key.to_sym]) } %>

2
config/locales/forms/2024/sales/sale_information.en.yml

@ -57,7 +57,7 @@ en:
check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: ""
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
question_text: "Was this part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
resale:
page_header: ""

2
config/locales/forms/2025/lettings/household_characteristics.en.yml

@ -7,7 +7,7 @@ en:
page_header: ""
check_answer_label: "Number of household members"
check_answer_prompt: "Enter total number of household members"
hint_text: "You can provide details for a maximum of 8 people."
hint_text: "You can answer up to 15 people. You will be asked to add details for a maximum of 8 people in the next questions."
question_text: "How many people live in the household for this letting?"
age1:

2
config/locales/forms/2025/lettings/household_situation.en.yml

@ -23,7 +23,7 @@ en:
reason:
check_answer_label: "Reason for leaving last settled home"
check_answer_prompt: ""
hint_text: "You told us this letting is a renewal. We have removed some options because of this."
hint_text: "The tenant’s ‘last settled home’ is their last long-standing home. For tenants who were in temporary accommodation, sleeping rough or otherwise homeless, their last settled home is where they were living previously.<br><br>You told us this letting is a renewal. We have removed some options because of this."
question_text: "What is the tenant’s main reason for the household leaving their last settled home?"
reasonother:
check_answer_label: ""

2
config/locales/forms/2025/sales/sale_information.en.yml

@ -53,7 +53,7 @@ en:
check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: "Back-to-back staircasing transactions are used as a way for shared owners who own less than 100% of their property to sell on the open market. It involves the shared owner purchasing the remaining share from their landlord and immediately selling 100% of the property to a buyer on the open market. The landlord is then reimbursed for the staircasing transaction through the proceeds of sale to the buyer."
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
question_text: "Was this part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
firststair:
page_header: ""

2
config/locales/forms/2026/lettings/household_characteristics.en.yml

@ -7,7 +7,7 @@ en:
page_header: ""
check_answer_label: "Number of household members"
check_answer_prompt: "Enter total number of household members"
hint_text: "You can provide details for a maximum of 8 people."
hint_text: "You can answer up to 15 people. You will be asked to add details for a maximum of 8 people in the next questions."
question_text: "How many people live in the household for this letting?"
age1:

2
config/locales/forms/2026/lettings/household_situation.en.yml

@ -23,7 +23,7 @@ en:
reason:
check_answer_label: "Reason for leaving last settled home"
check_answer_prompt: ""
hint_text: "You told us this letting is a renewal. We have removed some options because of this."
hint_text: "The tenant’s ‘last settled home’ is their last long-standing home. For tenants who were in temporary accommodation, sleeping rough or otherwise homeless, their last settled home is where they were living immediately before that period.<br><br>You told us this letting is a renewal. We have removed some options because of this."
question_text: "What is the tenant’s main reason for the household leaving their last settled home?"
reasonother:
check_answer_label: ""

9
config/locales/forms/2026/sales/guidance.en.yml

@ -55,3 +55,12 @@ en:
title: "What is a UPRN?"
content: "<p>The Unique Property Reference Number (UPRN) is a unique number system created by Ordnance Survey and used by housing providers and various industries across the UK. An example is 0010457355.</p>
<p>The UPRN may not be the same as the property reference assigned by your organisation.</p>"
building_height_class:
title: "What do these classifications mean?"
content: "<p>High-rise residential buildings are those containing 2 or more residential units and either:</p>
<ul class=\"govuk-list govuk-list--bullet\">
<li>have 7 or more stories</li>
<li>are at least 18 metres in height</li>
</ul>
<p>If unsure, answer based on the number of storeys.</p>"

4
config/locales/forms/2026/sales/property_information.en.yml

@ -60,10 +60,10 @@ en:
question_text: "What type of unit is the property?"
buildheightclass:
page_header: ""
page_header: "Building height classification"
check_answer_label: "Building height classification"
check_answer_prompt: ""
hint_text: "High-rise residential buildings are those containing 2 or more residential units and either have 7 or more storeys or are at least 18 metres in height. If unsure, answer based on the number of storeys."
hint_text: ""
question_text: "What is the building height classification?"
builtype:

2
config/locales/forms/2026/sales/sale_information.en.yml

@ -53,7 +53,7 @@ en:
check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: "Back-to-back staircasing transactions are used as a way for shared owners who own less than 100% of their property to sell on the open market. It involves the shared owner purchasing the remaining share from their landlord and immediately selling 100% of the property to a buyer on the open market. The landlord is then reimbursed for the staircasing transaction through the proceeds of sale to the buyer."
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
question_text: "Was this part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
firststair:
page_header: ""

5
db/migrate/20260420151627_add_force_reset_password_on_confirmation_to_users.rb

@ -0,0 +1,5 @@
class AddForceResetPasswordOnConfirmationToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :force_reset_password_on_confirmation, :boolean, default: false
end
end

3
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_03_05_095832) do
ActiveRecord::Schema[7.2].define(version: 2026_04_20_151627) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -924,6 +924,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_05_095832) do
t.datetime "discarded_at"
t.string "phone_extension"
t.datetime "values_updated_at"
t.boolean "force_reset_password_on_confirmation", default: false
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true

16
lib/tasks/correct_values_missing_max_for_2025_or_later_sales_logs.rake

@ -0,0 +1,16 @@
desc "Clears mortgage, purchase price (the 'value' field), monthly rent before staircasing and management fee values for sales logs in the database if they are over their new max"
task correct_values_missing_max_for_2025_or_later_sales_logs: :environment do
mortgage_incorrect_logs = SalesLog.filter_by_year_or_later(2025).where("mortgage > 999999")
value_incorrect_logs = SalesLog.filter_by_year_or_later(2025).where("value > 999999")
mrentprestaircasing_incorrect_logs = SalesLog.filter_by_year_or_later(2025).where("mrentprestaircasing > 9999")
management_fee_incorrect_logs = SalesLog.filter_by_year_or_later(2025).where("management_fee > 9999")
all_incorrect_logs = (mortgage_incorrect_logs + value_incorrect_logs + mrentprestaircasing_incorrect_logs + management_fee_incorrect_logs).uniq
puts "Correcting #{all_incorrect_logs.count} sales logs, #{all_incorrect_logs.map(&:id)}"
mortgage_incorrect_logs.update!(mortgage: nil)
value_incorrect_logs.update!(value: nil)
mrentprestaircasing_incorrect_logs.update!(mrentprestaircasing: nil)
management_fee_incorrect_logs.update!(management_fee: nil)
puts "Done"
end

13
lib/tasks/fix_sales_logs_with_invalid_initialpurchase_lasttransaction.rake

@ -0,0 +1,13 @@
desc "We tightened the validation in 2026 between initial purchase date, last transaction date and sale date so that no two can be equal and initial purchase date < last transaction date < sale date. To avoid invalid logs we clear lasttransaction if it equals saledate and if initialpurchase = lasttransaction we clear both"
task fix_sales_logs_with_invalid_initialpurchase_lasttransaction: :environment do
lasttransaction_equal_saledate_logs = SalesLog.filter_by_year_or_later(2026).where("lasttransaction = saledate")
initial_purchase_equal_lasttransaction_logs = SalesLog.filter_by_year_or_later(2026).where("initialpurchase = lasttransaction")
puts "Updating #{lasttransaction_equal_saledate_logs.count} logs where lasttransaction = saledate, #{lasttransaction_equal_saledate_logs.map(&:id)}"
lasttransaction_equal_saledate_logs.update!(lasttransaction: nil)
puts "Updating #{initial_purchase_equal_lasttransaction_logs.count} logs where initialpurchase = lasttransaction, #{initial_purchase_equal_lasttransaction_logs.map(&:id)}"
initial_purchase_equal_lasttransaction_logs.update!(initialpurchase: nil, lasttransaction: nil)
puts "Done"
end

6
spec/fixtures/files/lettings_log_csv_export_codes_24.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_codes_25.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_labels_25.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

4
spec/models/form/lettings/pages/net_income_value_check_spec.rb

@ -15,10 +15,6 @@ RSpec.describe Form::Lettings::Pages::NetIncomeValueCheck, type: :model do
expect(page.questions.map(&:id)).to eq(%w[net_income_value_check])
end
it "has the correct id" do
expect(page.id).to eq("net_income_value_check")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "net_income_soft_validation_triggered?" => true }])
end

29
spec/models/form/lettings/pages/person_known_spec.rb

@ -26,15 +26,12 @@ RSpec.describe Form::Lettings::Pages::PersonKnown, type: :model do
it "has correct depends_on" do
expect(page.depends_on).to eq(
[
{ "hhmemb" => 2 },
{ "hhmemb" => 3 },
{ "hhmemb" => 4 },
{ "hhmemb" => 5 },
{ "hhmemb" => 6 },
{ "hhmemb" => 7 },
{ "hhmemb" => 8 },
],
[{
"hhmemb" => {
"operator" => ">=",
"operand" => 2,
},
}],
)
end
end
@ -52,14 +49,12 @@ RSpec.describe Form::Lettings::Pages::PersonKnown, type: :model do
it "has correct depends_on" do
expect(page.depends_on).to eq(
[
{ "hhmemb" => 3 },
{ "hhmemb" => 4 },
{ "hhmemb" => 5 },
{ "hhmemb" => 6 },
{ "hhmemb" => 7 },
{ "hhmemb" => 8 },
],
[{
"hhmemb" => {
"operator" => ">=",
"operand" => 3,
},
}],
)
end
end

48
spec/models/form/lettings/subsections/household_characteristics_spec.rb

@ -28,6 +28,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
household_members
no_females_pregnant_household_lead_hhmemb_value_check
females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check
hhmemb_net_income_value_check
lead_tenant_age
no_females_pregnant_household_lead_age_value_check
females_in_soft_age_range_in_pregnant_household_lead_age_value_check
@ -47,6 +48,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
lead_tenant_working_situation
working_situation_lead_tenant_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_net_income_value_check
person_2_known
person_2_relationship_to_lead
relationship_2_partner_under_16_value_check
@ -57,6 +59,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_2_under_retirement_value_check
age_2_over_retirement_value_check
age_2_partner_under_16_value_check
age_2_net_income_value_check
person_2_gender_identity
no_females_pregnant_household_person_2_value_check
females_in_soft_age_range_in_pregnant_household_person_2_value_check
@ -64,6 +67,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_2_working_situation
working_situation_2_under_retirement_value_check
working_situation_2_over_retirement_value_check
working_situation_2_net_income_value_check
person_3_known
person_3_relationship_to_lead
relationship_3_partner_under_16_value_check
@ -74,6 +78,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_3_under_retirement_value_check
age_3_over_retirement_value_check
age_3_partner_under_16_value_check
age_3_net_income_value_check
person_3_gender_identity
no_females_pregnant_household_person_3_value_check
females_in_soft_age_range_in_pregnant_household_person_3_value_check
@ -81,6 +86,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_3_working_situation
working_situation_3_under_retirement_value_check
working_situation_3_over_retirement_value_check
working_situation_3_net_income_value_check
person_4_known
person_4_relationship_to_lead
relationship_4_partner_under_16_value_check
@ -91,6 +97,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_4_under_retirement_value_check
age_4_over_retirement_value_check
age_4_partner_under_16_value_check
age_4_net_income_value_check
person_4_gender_identity
no_females_pregnant_household_person_4_value_check
females_in_soft_age_range_in_pregnant_household_person_4_value_check
@ -98,6 +105,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_4_working_situation
working_situation_4_under_retirement_value_check
working_situation_4_over_retirement_value_check
working_situation_4_net_income_value_check
person_5_known
person_5_relationship_to_lead
relationship_5_partner_under_16_value_check
@ -108,6 +116,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_5_under_retirement_value_check
age_5_over_retirement_value_check
age_5_partner_under_16_value_check
age_5_net_income_value_check
person_5_gender_identity
no_females_pregnant_household_person_5_value_check
females_in_soft_age_range_in_pregnant_household_person_5_value_check
@ -115,6 +124,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_5_working_situation
working_situation_5_under_retirement_value_check
working_situation_5_over_retirement_value_check
working_situation_5_net_income_value_check
person_6_known
person_6_relationship_to_lead
relationship_6_partner_under_16_value_check
@ -125,6 +135,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_6_under_retirement_value_check
age_6_over_retirement_value_check
age_6_partner_under_16_value_check
age_6_net_income_value_check
person_6_gender_identity
no_females_pregnant_household_person_6_value_check
females_in_soft_age_range_in_pregnant_household_person_6_value_check
@ -132,6 +143,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_6_working_situation
working_situation_6_under_retirement_value_check
working_situation_6_over_retirement_value_check
working_situation_6_net_income_value_check
person_7_known
person_7_relationship_to_lead
relationship_7_partner_under_16_value_check
@ -142,6 +154,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_7_under_retirement_value_check
age_7_over_retirement_value_check
age_7_partner_under_16_value_check
age_7_net_income_value_check
person_7_gender_identity
no_females_pregnant_household_person_7_value_check
females_in_soft_age_range_in_pregnant_household_person_7_value_check
@ -149,6 +162,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_7_working_situation
working_situation_7_under_retirement_value_check
working_situation_7_over_retirement_value_check
working_situation_7_net_income_value_check
person_8_known
person_8_relationship_to_lead
relationship_8_partner_under_16_value_check
@ -159,6 +173,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_8_under_retirement_value_check
age_8_over_retirement_value_check
age_8_partner_under_16_value_check
age_8_net_income_value_check
person_8_gender_identity
no_females_pregnant_household_person_8_value_check
females_in_soft_age_range_in_pregnant_household_person_8_value_check
@ -166,6 +181,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_8_working_situation
working_situation_8_under_retirement_value_check
working_situation_8_over_retirement_value_check
working_situation_8_net_income_value_check
],
)
end
@ -182,6 +198,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
household_members
no_females_pregnant_household_lead_hhmemb_value_check
females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check
hhmemb_net_income_value_check
lead_tenant_age
no_females_pregnant_household_lead_age_value_check
females_in_soft_age_range_in_pregnant_household_lead_age_value_check
@ -201,6 +218,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
lead_tenant_working_situation
working_situation_lead_tenant_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_net_income_value_check
person_2_known
person_2_lead_partner
relationship_2_partner_under_16_value_check
@ -211,6 +229,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_2_under_retirement_value_check
age_2_over_retirement_value_check
age_2_partner_under_16_value_check
age_2_net_income_value_check
person_2_gender_identity
no_females_pregnant_household_person_2_value_check
females_in_soft_age_range_in_pregnant_household_person_2_value_check
@ -218,6 +237,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_2_working_situation
working_situation_2_under_retirement_value_check
working_situation_2_over_retirement_value_check
working_situation_2_net_income_value_check
person_3_known
person_3_lead_partner
relationship_3_partner_under_16_value_check
@ -228,6 +248,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_3_under_retirement_value_check
age_3_over_retirement_value_check
age_3_partner_under_16_value_check
age_3_net_income_value_check
person_3_gender_identity
no_females_pregnant_household_person_3_value_check
females_in_soft_age_range_in_pregnant_household_person_3_value_check
@ -235,6 +256,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_3_working_situation
working_situation_3_under_retirement_value_check
working_situation_3_over_retirement_value_check
working_situation_3_net_income_value_check
person_4_known
person_4_lead_partner
relationship_4_partner_under_16_value_check
@ -245,6 +267,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_4_under_retirement_value_check
age_4_over_retirement_value_check
age_4_partner_under_16_value_check
age_4_net_income_value_check
person_4_gender_identity
no_females_pregnant_household_person_4_value_check
females_in_soft_age_range_in_pregnant_household_person_4_value_check
@ -252,6 +275,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_4_working_situation
working_situation_4_under_retirement_value_check
working_situation_4_over_retirement_value_check
working_situation_4_net_income_value_check
person_5_known
person_5_lead_partner
relationship_5_partner_under_16_value_check
@ -262,6 +286,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_5_under_retirement_value_check
age_5_over_retirement_value_check
age_5_partner_under_16_value_check
age_5_net_income_value_check
person_5_gender_identity
no_females_pregnant_household_person_5_value_check
females_in_soft_age_range_in_pregnant_household_person_5_value_check
@ -269,6 +294,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_5_working_situation
working_situation_5_under_retirement_value_check
working_situation_5_over_retirement_value_check
working_situation_5_net_income_value_check
person_6_known
person_6_lead_partner
relationship_6_partner_under_16_value_check
@ -279,6 +305,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_6_under_retirement_value_check
age_6_over_retirement_value_check
age_6_partner_under_16_value_check
age_6_net_income_value_check
person_6_gender_identity
no_females_pregnant_household_person_6_value_check
females_in_soft_age_range_in_pregnant_household_person_6_value_check
@ -286,6 +313,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_6_working_situation
working_situation_6_under_retirement_value_check
working_situation_6_over_retirement_value_check
working_situation_6_net_income_value_check
person_7_known
person_7_lead_partner
relationship_7_partner_under_16_value_check
@ -296,6 +324,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_7_under_retirement_value_check
age_7_over_retirement_value_check
age_7_partner_under_16_value_check
age_7_net_income_value_check
person_7_gender_identity
no_females_pregnant_household_person_7_value_check
females_in_soft_age_range_in_pregnant_household_person_7_value_check
@ -303,6 +332,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_7_working_situation
working_situation_7_under_retirement_value_check
working_situation_7_over_retirement_value_check
working_situation_7_net_income_value_check
person_8_known
person_8_lead_partner
relationship_8_partner_under_16_value_check
@ -313,6 +343,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
age_8_under_retirement_value_check
age_8_over_retirement_value_check
age_8_partner_under_16_value_check
age_8_net_income_value_check
person_8_gender_identity
no_females_pregnant_household_person_8_value_check
females_in_soft_age_range_in_pregnant_household_person_8_value_check
@ -320,6 +351,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_8_working_situation
working_situation_8_under_retirement_value_check
working_situation_8_over_retirement_value_check
working_situation_8_net_income_value_check
],
)
end
@ -335,6 +367,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
%w[
household_members
no_household_member_likely_to_be_pregnant_hhmemb_check
hhmemb_net_income_value_check
lead_tenant_age
no_household_member_likely_to_be_pregnant_lead_age_check
age_lead_tenant_under_retirement_value_check
@ -354,8 +387,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_lead_tenant_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_long_term_illness_check
working_situation_lead_tenant_net_income_value_check
person_2_known
person_2_age
age_2_net_income_value_check
person_2_lead_partner
no_household_member_likely_to_be_pregnant_person_age_2_check
age_2_under_retirement_value_check
@ -368,8 +403,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_2_under_retirement_value_check
working_situation_2_over_retirement_value_check
working_situation_2_long_term_illness_check
working_situation_2_net_income_value_check
person_3_known
person_3_age
age_3_net_income_value_check
person_3_lead_partner
no_household_member_likely_to_be_pregnant_person_age_3_check
age_3_under_retirement_value_check
@ -382,8 +419,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_3_under_retirement_value_check
working_situation_3_over_retirement_value_check
working_situation_3_long_term_illness_check
working_situation_3_net_income_value_check
person_4_known
person_4_age
age_4_net_income_value_check
person_4_lead_partner
no_household_member_likely_to_be_pregnant_person_age_4_check
age_4_under_retirement_value_check
@ -396,8 +435,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_4_under_retirement_value_check
working_situation_4_over_retirement_value_check
working_situation_4_long_term_illness_check
working_situation_4_net_income_value_check
person_5_known
person_5_age
age_5_net_income_value_check
person_5_lead_partner
no_household_member_likely_to_be_pregnant_person_age_5_check
age_5_under_retirement_value_check
@ -410,8 +451,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_5_under_retirement_value_check
working_situation_5_over_retirement_value_check
working_situation_5_long_term_illness_check
working_situation_5_net_income_value_check
person_6_known
person_6_age
age_6_net_income_value_check
person_6_lead_partner
no_household_member_likely_to_be_pregnant_person_age_6_check
age_6_under_retirement_value_check
@ -424,8 +467,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_6_under_retirement_value_check
working_situation_6_over_retirement_value_check
working_situation_6_long_term_illness_check
working_situation_6_net_income_value_check
person_7_known
person_7_age
age_7_net_income_value_check
person_7_lead_partner
no_household_member_likely_to_be_pregnant_person_age_7_check
age_7_under_retirement_value_check
@ -438,8 +483,10 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_7_under_retirement_value_check
working_situation_7_over_retirement_value_check
working_situation_7_long_term_illness_check
working_situation_7_net_income_value_check
person_8_known
person_8_age
age_8_net_income_value_check
person_8_lead_partner
no_household_member_likely_to_be_pregnant_person_age_8_check
age_8_under_retirement_value_check
@ -452,6 +499,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
working_situation_8_under_retirement_value_check
working_situation_8_over_retirement_value_check
working_situation_8_long_term_illness_check
working_situation_8_net_income_value_check
],
)
end

4
spec/models/form/lettings/subsections/income_and_benefits_spec.rb

@ -24,7 +24,7 @@ RSpec.describe Form::Lettings::Subsections::IncomeAndBenefits, type: :model do
%w[
income_known
income_amount
net_income_value_check
income_amount_net_income_value_check
housing_benefit
benefits_proportion
rent_or_other_charges
@ -58,7 +58,7 @@ RSpec.describe Form::Lettings::Subsections::IncomeAndBenefits, type: :model do
%w[
income_known
income_amount
net_income_value_check
income_amount_net_income_value_check
housing_benefit
benefits_proportion
rent_or_other_charges

4
spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb

@ -1,11 +1,13 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::MonthlyRentStaircasingOwned, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: current_collection_start_date, start_year_2025_or_later?: true)) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)

4
spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb

@ -1,11 +1,13 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::MonthlyRentStaircasing, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: current_collection_start_year, start_year_2025_or_later?: true)) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)

2
spec/models/form/sales/pages/mortgage_amount_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::MortgageAmount, type: :model do
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false)) }
let(:subsection) { instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2025_or_later?: true, start_year_2026_or_later?: true)) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)

2
spec/models/form/sales/pages/purchase_price_outright_ownership_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::PurchasePriceOutrightOwnership, type: :model
let(:page_id) { "purchase_price" }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, id: "discounted_ownership_scheme", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false)) }
let(:subsection) { instance_double(Form::Subsection, id: "discounted_ownership_scheme", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2025_or_later?: true, start_year_2026_or_later?: false)) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)

2
spec/models/form/sales/pages/purchase_price_spec.rb

@ -8,7 +8,7 @@ RSpec.describe Form::Sales::Pages::PurchasePrice, type: :model do
let(:subsection) { instance_double(Form::Subsection) }
before do
allow(subsection).to receive_messages(form: instance_double(Form, start_year_2024_or_later?: false, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false), id: "discounted_ownership_scheme")
allow(subsection).to receive_messages(form: instance_double(Form, start_year_2024_or_later?: true, start_date: Time.zone.local(2023, 4, 1), start_year_2025_or_later?: true, start_year_2026_or_later?: false), id: "discounted_ownership_scheme")
end
it "has correct subsection" do

2
spec/models/form/sales/pages/value_shared_ownership_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::ValueSharedOwnership, type: :model do
let(:page_id) { "value_shared_ownership" }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false), id: "shared_ownership") }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2025_or_later?: true, start_year_2026_or_later?: false), id: "shared_ownership") }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)

4
spec/models/form/sales/questions/building_height_class_spec.rb

@ -36,4 +36,8 @@ RSpec.describe Form::Sales::Questions::BuildingHeightClass, type: :model do
it "has the correct question_number" do
expect(question.question_number).to eq(17)
end
it "has correct guidance partial" do
expect(question.top_guidance_partial).to eq("building_height_class")
end
end

9
spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb

@ -1,11 +1,14 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) }
let(:start_year_2025_or_later?) { true }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: current_collection_start_date, start_year_2025_or_later?: start_year_2025_or_later?))) }
it "has correct page" do
expect(question.page).to eq(page)
@ -34,4 +37,8 @@ RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :mode
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(9_999)
end
end

6
spec/models/form/sales/questions/mortgage_amount_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Questions::MortgageAmount, type: :model do
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false))) }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2025_or_later?: true, start_year_2026_or_later?: true))) }
it "has correct page" do
expect(question.page).to be(page)
@ -35,6 +35,10 @@ RSpec.describe Form::Sales::Questions::MortgageAmount, type: :model do
expect(question.min).to be(1)
end
it "has correct max" do
expect(question.max).to eq(999_999)
end
context "when the mortgage is not used" do
let(:log) { build(:sales_log, :completed, mortgageused: 2, deposit: nil) }

10
spec/models/form/sales/questions/purchase_price_spec.rb

@ -9,7 +9,7 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
let(:question_definition) { nil }
let(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?))) }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, id: "shared_ownership_initial_purchase", form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?, start_year_2025_or_later?: true))) }
it "has correct page" do
expect(question.page).to eq(page)
@ -65,6 +65,10 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end
context "with year 2026", metadata: { year: 26 } do
@ -74,5 +78,9 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
it "has correct min" do
expect(question.min).to eq(15_000)
end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end
end

14
spec/models/form/sales/questions/value_spec.rb

@ -9,11 +9,7 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do
let(:question_definition) { nil }
let(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false }
let(:page) { instance_double(Form::Page, id: "value_shared_ownership", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?), id: "shared_ownership")) }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
end
let(:page) { instance_double(Form::Page, id: "value_shared_ownership", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?, start_year_2025_or_later?: true), id: "shared_ownership")) }
it "has correct page" do
expect(question.page).to eq(page)
@ -45,6 +41,10 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end
context "with year 2026", metadata: { year: 26 } do
@ -54,5 +54,9 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do
it "has correct min" do
expect(question.min).to eq(15_000)
end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end
end

12
spec/models/validations/household_validations_spec.rb

@ -252,18 +252,18 @@ RSpec.describe Validations::HouseholdValidations do
record.hhmemb = 0
household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"])
.to include(match I18n.t("validations.shared.numeric.within_range", field: "Number of household members", min: 1, max: 8))
.to include(match I18n.t("validations.shared.numeric.within_range", field: "Number of household members", min: 1, max: 15))
end
it "validates that the number of household members cannot be more than 8" do
record.hhmemb = 9
it "validates that the number of household members cannot be more than 15" do
record.hhmemb = 16
household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"])
.to include(match I18n.t("validations.shared.numeric.within_range", field: "Number of household members", min: 1, max: 8))
.to include(match I18n.t("validations.shared.numeric.within_range", field: "Number of household members", min: 1, max: 15))
end
it "expects that the number of other household members is between the min and max" do
record.hhmemb = 5
it "expects that the number of household members is between the min and max" do
record.hhmemb = 11
household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"]).to be_empty
end

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

@ -252,12 +252,27 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end
context "when initial purchase date == saledate" do
let(:record) { build(:sales_log, initialpurchase: current_collection_start_date, saledate: current_collection_start_date) }
let(:record) { build(:sales_log, initialpurchase: collection_start_date_for_year(start_year), saledate: collection_start_date_for_year(start_year)) }
it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
expect(record.errors[:initialpurchase]).not_to be_present
it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:lasttransaction]).not_to be_present
end
end
context "and 2026", metadata: { year: 26 } do
let(:start_year) { 2026 }
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
end
end
@ -315,12 +330,27 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end
context "when last transaction date == saledate" do
let(:record) { build(:sales_log, lasttransaction: current_collection_start_date, saledate: current_collection_start_date) }
let(:record) { build(:sales_log, lasttransaction: collection_start_date_for_year(start_year), saledate: collection_start_date_for_year(start_year)) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
expect(record.errors[:lasttransaction]).not_to be_present
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 "and 2026", metadata: { year: 26 } do
let(:start_year) { 2026 }
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
end
@ -346,12 +376,27 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end
context "when last transaction date == initial purchase date" do
let(:record) { build(:sales_log, lasttransaction: current_collection_start_date, initialpurchase: current_collection_start_date) }
let(:record) { build(:sales_log, lasttransaction: collection_start_date_for_year(start_year), initialpurchase: collection_start_date_for_year(start_year), saledate: collection_start_date_for_year(start_year) + 1.day) }
it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record)
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
expect(record.errors[:lasttransaction]).not_to be_present
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 "and 2026", metadata: { year: 26 } do
let(:start_year) { 2026 }
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
end
end

17
spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb

@ -646,9 +646,9 @@ RSpec.describe BulkUpload::Lettings::Year2025::RowParser do
end
describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_45: 0 }) }
context "when a field has been marked as invalid" do
let(:attributes) { setup_section_params.merge({ field_45: 0 }) }
before do
parser.add_invalid_field("field_45")
end
@ -659,6 +659,19 @@ RSpec.describe BulkUpload::Lettings::Year2025::RowParser do
expect(parser.errors[:field_45]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "What is the lead tenant’s nationality?"))
end
end
context "when a field has been marked as invalid but it is not routed to" do
let(:attributes) { setup_section_params.merge({ field_117: 2 }) }
before do
parser.add_invalid_field("field_118")
end
it "does not set an error on that field" do
parser.valid?
expect(parser.errors[:field_118].size).to eq(0)
end
end
end
end
end

17
spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb

@ -538,9 +538,9 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
end
describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_46: 0 }) }
context "when a field has been marked as invalid" do
let(:attributes) { setup_section_params.merge({ field_46: 0 }) }
before do
parser.add_invalid_field("field_46")
end
@ -551,6 +551,19 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
expect(parser.errors[:field_46]).to include(match(I18n.t("validations.lettings.2026.bulk_upload.invalid_option", question: "What is the lead tenant’s nationality?")))
end
end
context "when a field has been marked as invalid but it is not routed to" do
let(:attributes) { setup_section_params.merge({ field_135: 2 }) }
before do
parser.add_invalid_field("field_136")
end
it "does not set an error on that field" do
parser.valid?
expect(parser.errors[:field_136].size).to eq(0)
end
end
end
end

28
spec/services/bulk_upload/sales/year2025/row_parser_spec.rb

@ -295,7 +295,7 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_29 field_36 field_44 field_48 field_52 field_56] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_58 field_64 field_75 field_70 field_72] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
@ -338,9 +338,10 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
end
describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_31: 0 }) }
context "when a field has been marked as invalid" do
# field_34 nationality is only shown if field_10 staircasing is no
let(:attributes) { setup_section_params.merge({ field_10: 2, field_31: 0 }) }
before do
parser.add_invalid_field("field_31")
end
@ -351,6 +352,19 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
expect(parser.errors[:field_31]).to include(match(I18n.t("validations.sales.2025.bulk_upload.invalid_option", question: "What is buyer 1’s nationality?")))
end
end
context "when a field has been marked as invalid but it is not routed to" do
let(:attributes) { setup_section_params.merge({ field_10: 1, field_31: 0 }) }
before do
parser.add_invalid_field("field_31")
end
it "does not set an error on that field" do
parser.valid?
expect(parser.errors[:field_31].size).to eq(0)
end
end
end
end
end
@ -1764,6 +1778,14 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
end
end
describe "#prevten" do
let(:attributes) { setup_section_params.merge({ field_58: "R" }) }
it "is correctly set" do
expect(parser.log.prevten).to be(0)
end
end
describe "#prevtenbuy2" do
let(:attributes) { setup_section_params.merge({ field_64: "R" }) }

28
spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

@ -301,7 +301,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_30 field_39 field_49 field_55 field_61 field_67] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_29 field_38 field_48 field_54 field_60 field_66 field_77 field_88 field_83 field_85 field_103 field_107 field_125 field_126 field_133 field_136] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_29 field_38 field_48 field_54 field_60 field_66 field_71 field_77 field_88 field_83 field_85 field_103 field_107 field_125 field_126 field_133 field_136] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
@ -344,9 +344,10 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
end
describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_34: 0 }) }
context "when a field has been marked as invalid" do
# field_34 nationality is only shown if field_10 staircasing is no
let(:attributes) { setup_section_params.merge({ field_10: 2, field_34: 0 }) }
before do
parser.add_invalid_field("field_34")
end
@ -357,6 +358,19 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
expect(parser.errors[:field_34]).to include(match(I18n.t("validations.sales.2026.bulk_upload.invalid_option", question: "What is buyer 1's nationality?")))
end
end
context "when a field has been marked as invalid but it is not routed to" do
let(:attributes) { setup_section_params.merge({ field_10: 1, field_34: 0 }) }
before do
parser.add_invalid_field("field_34")
end
it "does not set an error on that field" do
parser.valid?
expect(parser.errors[:field_34].size).to eq(0)
end
end
end
end
end
@ -1826,6 +1840,14 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
end
end
describe "#prevten" do
let(:attributes) { setup_section_params.merge({ field_71: "R" }) }
it "is correctly set" do
expect(parser.log.prevten).to be(0)
end
end
describe "#prevtenbuy2" do
let(:attributes) { setup_section_params.merge({ field_77: "R" }) }

Loading…
Cancel
Save