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. 53
      spec/models/validations/sales/sale_information_validations_spec.rb
  63. 15
      spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb
  64. 15
      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? yield resource if block_given?
if resource.errors.empty? 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) token = resource.send(:set_reset_password_token)
redirect_to "#{edit_user_password_url}?reset_password_token=#{token}&confirmation=true" redirect_to "#{edit_user_password_url}?reset_password_token=#{token}&confirmation=true"
else else

1
app/controllers/auth/passwords_controller.rb

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

2
app/models/derived_variables/lettings_log_variables.rb

@ -339,7 +339,7 @@ private
def infer_only_partner!(partner_number) def infer_only_partner!(partner_number)
return unless hhmemb return unless hhmemb
(2..hhmemb).each do |i| (2..people_with_details).each do |i|
next if i == partner_number next if i == partner_number
if ["P", nil].include?(public_send("relat#{i}")) 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 class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
def initialize(id, hsh, subsection) def initialize(id, hsh, subsection, person_index: nil)
super super(id, hsh, subsection)
@id = "net_income_value_check"
@copy_key = "lettings.soft_validations.net_income_value_check" @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 = { @title_text = {
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text",
"arguments" => [ "arguments" => [
@ -32,6 +32,23 @@ class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
} }
end 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 def questions
@questions ||= [Form::Lettings::Questions::NetIncomeValueCheck.new(nil, nil, self)] @questions ||= [Form::Lettings::Questions::NetIncomeValueCheck.new(nil, nil, self)]
end 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:) def initialize(id, hsh, subsection, person_index:)
super(id, hsh, subsection) super(id, hsh, subsection)
@id = "person_#{person_index}_known" @id = "person_#{person_index}_known"
@depends_on = (person_index..8).map { |index| { "hhmemb" => index } }
@person_index = person_index @person_index = person_index
@depends_on = depends_on
end end
def questions def questions
@questions ||= [Form::Lettings::Questions::DetailsKnown.new(nil, nil, self, person_index: @person_index)] @questions ||= [Form::Lettings::Questions::DetailsKnown.new(nil, nil, self, person_index: @person_index)]
end end
def depends_on
[{ "hhmemb" => {
"operator" => ">=",
"operand" => @person_index,
} }]
end
end end

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

@ -5,7 +5,7 @@ class Form::Lettings::Questions::Hhmemb < ::Form::Question
@type = "numeric" @type = "numeric"
@width = 2 @width = 2
@check_answers_card_number = 0 @check_answers_card_number = 0
@max = 8 @max = 15
@min = 1 @min = 1
@step = 1 @step = 1
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR) @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::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::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::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::LeadTenantAge.new(nil, nil, self),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?), (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?), (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::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::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::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: 2),
*person_questions(person_index: 3), *person_questions(person_index: 3),
*person_questions(person_index: 4), *person_questions(person_index: 4),
@ -50,10 +52,14 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
def person_questions(person_index:) def person_questions(person_index:)
[ [
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, 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::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:), 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::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::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::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::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?), (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::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::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::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::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::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?), (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::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::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::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::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::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::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::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 end

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

@ -10,7 +10,7 @@ class Form::Lettings::Subsections::IncomeAndBenefits < ::Form::Subsection
@pages ||= [ @pages ||= [
Form::Lettings::Pages::IncomeKnown.new(nil, nil, self), Form::Lettings::Pages::IncomeKnown.new(nil, nil, self),
Form::Lettings::Pages::IncomeAmount.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::HousingBenefit.new("housing_benefit", nil, self),
Form::Lettings::Pages::BenefitsProportion.new("benefits_proportion", nil, self), Form::Lettings::Pages::BenefitsProportion.new("benefits_proportion", nil, self),
Form::Lettings::Pages::RentOrOtherCharges.new(nil, 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 super
@id = "buildheightclass" @id = "buildheightclass"
@type = "radio" @type = "radio"
@top_guidance_partial = "building_height_class"
@answer_options = ANSWER_OPTIONS @answer_options = ANSWER_OPTIONS
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR) @question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR)
end 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" @copy_key = "sales.sale_information.management_fee.management_fee"
@type = "numeric" @type = "numeric"
@min = 1 @min = 1
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01 @step = 0.01
@width = 5 @width = 5
@prefix = "£" @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" @copy_key = "sales.sale_information.mrent_staircasing.prestaircasing"
@type = "numeric" @type = "numeric"
@min = 0 @min = 0
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01 @step = 0.01
@width = 5 @width = 5
@prefix = "£" @prefix = "£"

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

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

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

@ -4,6 +4,7 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
@id = "value" @id = "value"
@type = "numeric" @type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0 @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 @step = form.start_year_2026_or_later? ? 1 : 0.01 # 0.01 was a mistake that was fixed in 2026
@width = 5 @width = 5
@prefix = "£" @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" @copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric" @type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0 @min = form.start_year_2026_or_later? ? 15_000 : 0
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = 1 @step = 1
@width = 10 @width = 10
@prefix = "£" @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 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 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 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 = { RENT_TYPE = {
social_rent: 0, social_rent: 0,
affordable_rent: 1, affordable_rent: 1,
@ -284,7 +285,7 @@ class LettingsLog < Log
range = ALLOWED_INCOME_RANGES[ecstat1].clone range = ALLOWED_INCOME_RANGES[ecstat1].clone
if hhmemb > 1 if hhmemb > 1
(2..hhmemb).each do |person_index| (2..people_with_details).each do |person_index|
ecstat = self["ecstat#{person_index}"] ecstat = self["ecstat#{person_index}"]
if ecstat.nil? if ecstat.nil?

8
app/models/log.rb

@ -204,6 +204,14 @@ class Log < ApplicationRecord
false false
end 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? def ethnic_refused?
ethnic_group == 17 ethnic_group == 17
end end

1
app/models/sales_log.rb

@ -104,6 +104,7 @@ class SalesLog < Log
OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze 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 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? def lettings?
false false

2
app/models/user.rb

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

4
app/models/validations/financial_validations.rb

@ -41,7 +41,7 @@ module Validations::FinancialValidations
:over_hard_max, :over_hard_max,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_over_hard_max", earnings: format_as_currency(record.earnings), frequency:), 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( record.errors.add(
"ecstat#{n}", "ecstat#{n}",
:over_hard_max, :over_hard_max,
@ -70,7 +70,7 @@ module Validations::FinancialValidations
:under_hard_min, :under_hard_min,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_under_hard_min", earnings: format_as_currency(record.earnings), frequency:), 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( record.errors.add(
"ecstat#{n}", "ecstat#{n}",
:under_hard_min, :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") record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980")
end 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 :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") record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_initial_purchase_date")
end end
@ -55,11 +55,11 @@ module Validations::Sales::SaleInformationValidations
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_1980") record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_1980")
end 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 :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") record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_last_transaction_date")
end 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 :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") record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_initial_purchase")
end end

35
app/models/validations/soft_validations.rb

@ -208,8 +208,7 @@ module Validations::SoftValidations
def multiple_partners? def multiple_partners?
return unless hhmemb return unless hhmemb
max_person_with_details = sales? ? [hhmemb, 6].min : [hhmemb, 8].min (2..people_with_details).many? { |n| public_send("relat#{n}") == "P" }
(2..max_person_with_details).many? { |n| public_send("relat#{n}") == "P" }
end end
def at_least_one_working_situation_is_sickness_and_household_sickness_is_no? def at_least_one_working_situation_is_sickness_and_household_sickness_is_no?
@ -219,22 +218,18 @@ module Validations::SoftValidations
private private
def all_tenants_age_and_gender_information_completed? 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? return false unless all_tenants_gender_information_completed?
person_count = hhmemb || 8 (1..people_with_details).all? do |n|
(1..person_count).all? do |n|
public_send("age#{n}").present? && public_send("age#{n}_known").present? && public_send("age#{n}_known").zero? public_send("age#{n}").present? && public_send("age#{n}_known").present? && public_send("age#{n}_known").zero?
end end
end end
def all_tenants_gender_information_completed? def all_tenants_gender_information_completed?
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|
tenant_gender_information_completed?(n) tenant_gender_information_completed?(n)
end end
end end
@ -258,27 +253,21 @@ private
end end
def any_non_male_in_expected_pregnancy_age_range(min, max) def any_non_male_in_expected_pregnancy_age_range(min, max)
person_count = hhmemb || 8 (1..people_with_details).any? do |n|
(1..person_count).any? do |n|
person_in_expected_pregnancy_age_range(n, min, max) && person_is_non_male(n) person_in_expected_pregnancy_age_range(n, min, max) && person_is_non_male(n)
end end
end end
def non_males_in_the_household? def non_males_in_the_household?
person_count = hhmemb || 8 (1..people_with_details).any? do |n|
(1..person_count).any? do |n|
person_is_non_male(n) person_is_non_male(n)
end end
end end
def all_male_tenants_in_the_household? 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..people_with_details).all? do |n|
(1..person_count).all? do |n|
person_is_male(n) person_is_male(n)
end end
end end
@ -344,11 +333,7 @@ private
end end
def at_least_one_person_working_situation_is_illness? def at_least_one_person_working_situation_is_illness?
return if hhmemb.present? && hhmemb > 8 (1..people_with_details).any? { |n| public_send("ecstat#{n}") == 8 }
person_count = hhmemb || 8
(1..person_count).any? { |n| public_send("ecstat#{n}") == 8 }
end end
def no_one_in_household_with_illness? 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_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_address_fields, on: :after_log, unless: -> { supported_housing? }
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log validate :validate_prevten_value_when_renewal, on: :after_log
@ -506,6 +505,8 @@ class BulkUpload::Lettings::Year2025::RowParser
end end
end end
validate_incomplete_soft_validations
add_errors_for_invalid_fields add_errors_for_invalid_fields
@valid = errors.blank? @valid = errors.blank?
@ -1035,6 +1036,14 @@ private
def add_errors_for_invalid_fields def add_errors_for_invalid_fields
invalid_fields.each do |field| 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.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])) errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end 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_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, 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_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log validate :validate_prevten_value_when_renewal, on: :after_log
@ -541,6 +540,8 @@ class BulkUpload::Lettings::Year2026::RowParser
end end
end end
validate_incomplete_soft_validations
add_errors_for_invalid_fields add_errors_for_invalid_fields
@valid = errors.blank? @valid = errors.blank?
@ -1113,6 +1114,14 @@ private
def add_errors_for_invalid_fields def add_errors_for_invalid_fields
invalid_fields.each do |field| 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.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])) errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end 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_52, # Gender identity of person 5
:field_56, # Gender identity of person 6 :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_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? :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_56, :string
attribute :field_57, :integer attribute :field_57, :integer
attribute :field_58, :integer attribute :field_58, :string
attribute :field_59, :integer attribute :field_59, :integer
attribute :field_60, :string attribute :field_60, :string
attribute :field_61, :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_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, 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_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, 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? } 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
end end
validate_incomplete_soft_validations
add_errors_for_invalid_fields add_errors_for_invalid_fields
errors.blank? errors.blank?
@ -564,6 +565,15 @@ private
end end
end end
def prevten
case field_58
when "R"
0
else
field_58
end
end
def prevtenbuy2 def prevtenbuy2
case field_64 case field_64
when "R" when "R"
@ -689,6 +699,14 @@ private
def add_errors_for_invalid_fields def add_errors_for_invalid_fields
invalid_fields.each do |field| 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.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])) errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end end
@ -902,7 +920,7 @@ private
attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/) attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/)
attributes["prevown"] = field_76 attributes["prevown"] = field_76
attributes["prevten"] = field_58 attributes["prevten"] = prevten
attributes["prevloc"] = field_62 attributes["prevloc"] = field_62
attributes["previous_la_known"] = previous_la_known attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known attributes["ppcodenk"] = previous_postcode_known
@ -1283,7 +1301,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2 def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership? 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 1
else else
2 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) } 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| 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(" ") 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) errors.add(field, message: error_message, category: :soft_validation)
end 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_61, # Person 5's sex, as registered at birth
:field_67, # Person 6'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_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? :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_69, :string
attribute :field_70, :integer attribute :field_70, :integer
attribute :field_71, :integer attribute :field_71, :string
attribute :field_72, :integer attribute :field_72, :integer
attribute :field_73, :string attribute :field_73, :string
attribute :field_74, :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_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, 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_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log validate :validate_address_fields, on: :after_log
@ -560,6 +560,8 @@ class BulkUpload::Sales::Year2026::RowParser
end end
end end
validate_incomplete_soft_validations
add_errors_for_invalid_fields add_errors_for_invalid_fields
errors.blank? errors.blank?
@ -621,6 +623,15 @@ private
end end
end end
def prevten
case field_71
when "R"
0
else
field_71
end
end
def prevtenbuy2 def prevtenbuy2
case field_77 case field_77
when "R" when "R"
@ -750,6 +761,14 @@ private
def add_errors_for_invalid_fields def add_errors_for_invalid_fields
invalid_fields.each do |field| 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.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])) errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end end
@ -995,7 +1014,7 @@ private
attributes["savings"] = field_88.to_i if attributes["savingsnk"]&.zero? && field_88&.match(/\A\d+\z/) attributes["savings"] = field_88.to_i if attributes["savingsnk"]&.zero? && field_88&.match(/\A\d+\z/)
attributes["prevown"] = field_89 attributes["prevown"] = field_89
attributes["prevten"] = field_71 attributes["prevten"] = prevten
attributes["prevloc"] = field_75 attributes["prevloc"] = field_75
attributes["previous_la_known"] = previous_la_known attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known attributes["ppcodenk"] = previous_postcode_known
@ -1416,7 +1435,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2 def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership? 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 1
else else
2 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) } 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| 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(" ") 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) errors.add(field, message: error_message, category: :soft_validation)
end 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 : "" } %> options: { disabled: [""], selected: @organisation_id ? answer_options.first : "" } %>
<% end %> <% 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]) } %> <% 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_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" check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: "" 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: resale:
page_header: "" page_header: ""

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

@ -7,7 +7,7 @@ en:
page_header: "" page_header: ""
check_answer_label: "Number of household members" check_answer_label: "Number of household members"
check_answer_prompt: "Enter total 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?" question_text: "How many people live in the household for this letting?"
age1: age1:

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

@ -23,7 +23,7 @@ en:
reason: reason:
check_answer_label: "Reason for leaving last settled home" check_answer_label: "Reason for leaving last settled home"
check_answer_prompt: "" 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?" question_text: "What is the tenant’s main reason for the household leaving their last settled home?"
reasonother: reasonother:
check_answer_label: "" 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_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" 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." 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: firststair:
page_header: "" page_header: ""

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

@ -7,7 +7,7 @@ en:
page_header: "" page_header: ""
check_answer_label: "Number of household members" check_answer_label: "Number of household members"
check_answer_prompt: "Enter total 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?" question_text: "How many people live in the household for this letting?"
age1: age1:

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

@ -23,7 +23,7 @@ en:
reason: reason:
check_answer_label: "Reason for leaving last settled home" check_answer_label: "Reason for leaving last settled home"
check_answer_prompt: "" 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?" question_text: "What is the tenant’s main reason for the household leaving their last settled home?"
reasonother: reasonother:
check_answer_label: "" check_answer_label: ""

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

@ -55,3 +55,12 @@ en:
title: "What is a UPRN?" 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> 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>" <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?" question_text: "What type of unit is the property?"
buildheightclass: buildheightclass:
page_header: "" page_header: "Building height classification"
check_answer_label: "Building height classification" check_answer_label: "Building height classification"
check_answer_prompt: "" 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?" question_text: "What is the building height classification?"
builtype: 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_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" 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." 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: firststair:
page_header: "" 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -924,6 +924,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_05_095832) do
t.datetime "discarded_at" t.datetime "discarded_at"
t.string "phone_extension" t.string "phone_extension"
t.datetime "values_updated_at" 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 ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", 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 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]) expect(page.questions.map(&:id)).to eq(%w[net_income_value_check])
end end
it "has the correct id" do
expect(page.id).to eq("net_income_value_check")
end
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq([{ "net_income_soft_validation_triggered?" => true }]) expect(page.depends_on).to eq([{ "net_income_soft_validation_triggered?" => true }])
end 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 it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[ [{
{ "hhmemb" => 2 }, "hhmemb" => {
{ "hhmemb" => 3 }, "operator" => ">=",
{ "hhmemb" => 4 }, "operand" => 2,
{ "hhmemb" => 5 }, },
{ "hhmemb" => 6 }, }],
{ "hhmemb" => 7 },
{ "hhmemb" => 8 },
],
) )
end end
end end
@ -52,14 +49,12 @@ RSpec.describe Form::Lettings::Pages::PersonKnown, type: :model do
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[ [{
{ "hhmemb" => 3 }, "hhmemb" => {
{ "hhmemb" => 4 }, "operator" => ">=",
{ "hhmemb" => 5 }, "operand" => 3,
{ "hhmemb" => 6 }, },
{ "hhmemb" => 7 }, }],
{ "hhmemb" => 8 },
],
) )
end end
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 household_members
no_females_pregnant_household_lead_hhmemb_value_check no_females_pregnant_household_lead_hhmemb_value_check
females_in_soft_age_range_in_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 lead_tenant_age
no_females_pregnant_household_lead_age_value_check no_females_pregnant_household_lead_age_value_check
females_in_soft_age_range_in_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 lead_tenant_working_situation
working_situation_lead_tenant_under_retirement_value_check working_situation_lead_tenant_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_net_income_value_check
person_2_known person_2_known
person_2_relationship_to_lead person_2_relationship_to_lead
relationship_2_partner_under_16_value_check 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_under_retirement_value_check
age_2_over_retirement_value_check age_2_over_retirement_value_check
age_2_partner_under_16_value_check age_2_partner_under_16_value_check
age_2_net_income_value_check
person_2_gender_identity person_2_gender_identity
no_females_pregnant_household_person_2_value_check no_females_pregnant_household_person_2_value_check
females_in_soft_age_range_in_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 person_2_working_situation
working_situation_2_under_retirement_value_check working_situation_2_under_retirement_value_check
working_situation_2_over_retirement_value_check working_situation_2_over_retirement_value_check
working_situation_2_net_income_value_check
person_3_known person_3_known
person_3_relationship_to_lead person_3_relationship_to_lead
relationship_3_partner_under_16_value_check 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_under_retirement_value_check
age_3_over_retirement_value_check age_3_over_retirement_value_check
age_3_partner_under_16_value_check age_3_partner_under_16_value_check
age_3_net_income_value_check
person_3_gender_identity person_3_gender_identity
no_females_pregnant_household_person_3_value_check no_females_pregnant_household_person_3_value_check
females_in_soft_age_range_in_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 person_3_working_situation
working_situation_3_under_retirement_value_check working_situation_3_under_retirement_value_check
working_situation_3_over_retirement_value_check working_situation_3_over_retirement_value_check
working_situation_3_net_income_value_check
person_4_known person_4_known
person_4_relationship_to_lead person_4_relationship_to_lead
relationship_4_partner_under_16_value_check 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_under_retirement_value_check
age_4_over_retirement_value_check age_4_over_retirement_value_check
age_4_partner_under_16_value_check age_4_partner_under_16_value_check
age_4_net_income_value_check
person_4_gender_identity person_4_gender_identity
no_females_pregnant_household_person_4_value_check no_females_pregnant_household_person_4_value_check
females_in_soft_age_range_in_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 person_4_working_situation
working_situation_4_under_retirement_value_check working_situation_4_under_retirement_value_check
working_situation_4_over_retirement_value_check working_situation_4_over_retirement_value_check
working_situation_4_net_income_value_check
person_5_known person_5_known
person_5_relationship_to_lead person_5_relationship_to_lead
relationship_5_partner_under_16_value_check 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_under_retirement_value_check
age_5_over_retirement_value_check age_5_over_retirement_value_check
age_5_partner_under_16_value_check age_5_partner_under_16_value_check
age_5_net_income_value_check
person_5_gender_identity person_5_gender_identity
no_females_pregnant_household_person_5_value_check no_females_pregnant_household_person_5_value_check
females_in_soft_age_range_in_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 person_5_working_situation
working_situation_5_under_retirement_value_check working_situation_5_under_retirement_value_check
working_situation_5_over_retirement_value_check working_situation_5_over_retirement_value_check
working_situation_5_net_income_value_check
person_6_known person_6_known
person_6_relationship_to_lead person_6_relationship_to_lead
relationship_6_partner_under_16_value_check 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_under_retirement_value_check
age_6_over_retirement_value_check age_6_over_retirement_value_check
age_6_partner_under_16_value_check age_6_partner_under_16_value_check
age_6_net_income_value_check
person_6_gender_identity person_6_gender_identity
no_females_pregnant_household_person_6_value_check no_females_pregnant_household_person_6_value_check
females_in_soft_age_range_in_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 person_6_working_situation
working_situation_6_under_retirement_value_check working_situation_6_under_retirement_value_check
working_situation_6_over_retirement_value_check working_situation_6_over_retirement_value_check
working_situation_6_net_income_value_check
person_7_known person_7_known
person_7_relationship_to_lead person_7_relationship_to_lead
relationship_7_partner_under_16_value_check 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_under_retirement_value_check
age_7_over_retirement_value_check age_7_over_retirement_value_check
age_7_partner_under_16_value_check age_7_partner_under_16_value_check
age_7_net_income_value_check
person_7_gender_identity person_7_gender_identity
no_females_pregnant_household_person_7_value_check no_females_pregnant_household_person_7_value_check
females_in_soft_age_range_in_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 person_7_working_situation
working_situation_7_under_retirement_value_check working_situation_7_under_retirement_value_check
working_situation_7_over_retirement_value_check working_situation_7_over_retirement_value_check
working_situation_7_net_income_value_check
person_8_known person_8_known
person_8_relationship_to_lead person_8_relationship_to_lead
relationship_8_partner_under_16_value_check 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_under_retirement_value_check
age_8_over_retirement_value_check age_8_over_retirement_value_check
age_8_partner_under_16_value_check age_8_partner_under_16_value_check
age_8_net_income_value_check
person_8_gender_identity person_8_gender_identity
no_females_pregnant_household_person_8_value_check no_females_pregnant_household_person_8_value_check
females_in_soft_age_range_in_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 person_8_working_situation
working_situation_8_under_retirement_value_check working_situation_8_under_retirement_value_check
working_situation_8_over_retirement_value_check working_situation_8_over_retirement_value_check
working_situation_8_net_income_value_check
], ],
) )
end end
@ -182,6 +198,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
household_members household_members
no_females_pregnant_household_lead_hhmemb_value_check no_females_pregnant_household_lead_hhmemb_value_check
females_in_soft_age_range_in_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 lead_tenant_age
no_females_pregnant_household_lead_age_value_check no_females_pregnant_household_lead_age_value_check
females_in_soft_age_range_in_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 lead_tenant_working_situation
working_situation_lead_tenant_under_retirement_value_check working_situation_lead_tenant_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_net_income_value_check
person_2_known person_2_known
person_2_lead_partner person_2_lead_partner
relationship_2_partner_under_16_value_check 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_under_retirement_value_check
age_2_over_retirement_value_check age_2_over_retirement_value_check
age_2_partner_under_16_value_check age_2_partner_under_16_value_check
age_2_net_income_value_check
person_2_gender_identity person_2_gender_identity
no_females_pregnant_household_person_2_value_check no_females_pregnant_household_person_2_value_check
females_in_soft_age_range_in_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 person_2_working_situation
working_situation_2_under_retirement_value_check working_situation_2_under_retirement_value_check
working_situation_2_over_retirement_value_check working_situation_2_over_retirement_value_check
working_situation_2_net_income_value_check
person_3_known person_3_known
person_3_lead_partner person_3_lead_partner
relationship_3_partner_under_16_value_check 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_under_retirement_value_check
age_3_over_retirement_value_check age_3_over_retirement_value_check
age_3_partner_under_16_value_check age_3_partner_under_16_value_check
age_3_net_income_value_check
person_3_gender_identity person_3_gender_identity
no_females_pregnant_household_person_3_value_check no_females_pregnant_household_person_3_value_check
females_in_soft_age_range_in_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 person_3_working_situation
working_situation_3_under_retirement_value_check working_situation_3_under_retirement_value_check
working_situation_3_over_retirement_value_check working_situation_3_over_retirement_value_check
working_situation_3_net_income_value_check
person_4_known person_4_known
person_4_lead_partner person_4_lead_partner
relationship_4_partner_under_16_value_check 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_under_retirement_value_check
age_4_over_retirement_value_check age_4_over_retirement_value_check
age_4_partner_under_16_value_check age_4_partner_under_16_value_check
age_4_net_income_value_check
person_4_gender_identity person_4_gender_identity
no_females_pregnant_household_person_4_value_check no_females_pregnant_household_person_4_value_check
females_in_soft_age_range_in_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 person_4_working_situation
working_situation_4_under_retirement_value_check working_situation_4_under_retirement_value_check
working_situation_4_over_retirement_value_check working_situation_4_over_retirement_value_check
working_situation_4_net_income_value_check
person_5_known person_5_known
person_5_lead_partner person_5_lead_partner
relationship_5_partner_under_16_value_check 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_under_retirement_value_check
age_5_over_retirement_value_check age_5_over_retirement_value_check
age_5_partner_under_16_value_check age_5_partner_under_16_value_check
age_5_net_income_value_check
person_5_gender_identity person_5_gender_identity
no_females_pregnant_household_person_5_value_check no_females_pregnant_household_person_5_value_check
females_in_soft_age_range_in_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 person_5_working_situation
working_situation_5_under_retirement_value_check working_situation_5_under_retirement_value_check
working_situation_5_over_retirement_value_check working_situation_5_over_retirement_value_check
working_situation_5_net_income_value_check
person_6_known person_6_known
person_6_lead_partner person_6_lead_partner
relationship_6_partner_under_16_value_check 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_under_retirement_value_check
age_6_over_retirement_value_check age_6_over_retirement_value_check
age_6_partner_under_16_value_check age_6_partner_under_16_value_check
age_6_net_income_value_check
person_6_gender_identity person_6_gender_identity
no_females_pregnant_household_person_6_value_check no_females_pregnant_household_person_6_value_check
females_in_soft_age_range_in_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 person_6_working_situation
working_situation_6_under_retirement_value_check working_situation_6_under_retirement_value_check
working_situation_6_over_retirement_value_check working_situation_6_over_retirement_value_check
working_situation_6_net_income_value_check
person_7_known person_7_known
person_7_lead_partner person_7_lead_partner
relationship_7_partner_under_16_value_check 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_under_retirement_value_check
age_7_over_retirement_value_check age_7_over_retirement_value_check
age_7_partner_under_16_value_check age_7_partner_under_16_value_check
age_7_net_income_value_check
person_7_gender_identity person_7_gender_identity
no_females_pregnant_household_person_7_value_check no_females_pregnant_household_person_7_value_check
females_in_soft_age_range_in_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 person_7_working_situation
working_situation_7_under_retirement_value_check working_situation_7_under_retirement_value_check
working_situation_7_over_retirement_value_check working_situation_7_over_retirement_value_check
working_situation_7_net_income_value_check
person_8_known person_8_known
person_8_lead_partner person_8_lead_partner
relationship_8_partner_under_16_value_check 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_under_retirement_value_check
age_8_over_retirement_value_check age_8_over_retirement_value_check
age_8_partner_under_16_value_check age_8_partner_under_16_value_check
age_8_net_income_value_check
person_8_gender_identity person_8_gender_identity
no_females_pregnant_household_person_8_value_check no_females_pregnant_household_person_8_value_check
females_in_soft_age_range_in_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 person_8_working_situation
working_situation_8_under_retirement_value_check working_situation_8_under_retirement_value_check
working_situation_8_over_retirement_value_check working_situation_8_over_retirement_value_check
working_situation_8_net_income_value_check
], ],
) )
end end
@ -335,6 +367,7 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
%w[ %w[
household_members household_members
no_household_member_likely_to_be_pregnant_hhmemb_check no_household_member_likely_to_be_pregnant_hhmemb_check
hhmemb_net_income_value_check
lead_tenant_age lead_tenant_age
no_household_member_likely_to_be_pregnant_lead_age_check no_household_member_likely_to_be_pregnant_lead_age_check
age_lead_tenant_under_retirement_value_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_under_retirement_value_check
working_situation_lead_tenant_over_retirement_value_check working_situation_lead_tenant_over_retirement_value_check
working_situation_lead_tenant_long_term_illness_check working_situation_lead_tenant_long_term_illness_check
working_situation_lead_tenant_net_income_value_check
person_2_known person_2_known
person_2_age person_2_age
age_2_net_income_value_check
person_2_lead_partner person_2_lead_partner
no_household_member_likely_to_be_pregnant_person_age_2_check no_household_member_likely_to_be_pregnant_person_age_2_check
age_2_under_retirement_value_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_under_retirement_value_check
working_situation_2_over_retirement_value_check working_situation_2_over_retirement_value_check
working_situation_2_long_term_illness_check working_situation_2_long_term_illness_check
working_situation_2_net_income_value_check
person_3_known person_3_known
person_3_age person_3_age
age_3_net_income_value_check
person_3_lead_partner person_3_lead_partner
no_household_member_likely_to_be_pregnant_person_age_3_check no_household_member_likely_to_be_pregnant_person_age_3_check
age_3_under_retirement_value_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_under_retirement_value_check
working_situation_3_over_retirement_value_check working_situation_3_over_retirement_value_check
working_situation_3_long_term_illness_check working_situation_3_long_term_illness_check
working_situation_3_net_income_value_check
person_4_known person_4_known
person_4_age person_4_age
age_4_net_income_value_check
person_4_lead_partner person_4_lead_partner
no_household_member_likely_to_be_pregnant_person_age_4_check no_household_member_likely_to_be_pregnant_person_age_4_check
age_4_under_retirement_value_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_under_retirement_value_check
working_situation_4_over_retirement_value_check working_situation_4_over_retirement_value_check
working_situation_4_long_term_illness_check working_situation_4_long_term_illness_check
working_situation_4_net_income_value_check
person_5_known person_5_known
person_5_age person_5_age
age_5_net_income_value_check
person_5_lead_partner person_5_lead_partner
no_household_member_likely_to_be_pregnant_person_age_5_check no_household_member_likely_to_be_pregnant_person_age_5_check
age_5_under_retirement_value_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_under_retirement_value_check
working_situation_5_over_retirement_value_check working_situation_5_over_retirement_value_check
working_situation_5_long_term_illness_check working_situation_5_long_term_illness_check
working_situation_5_net_income_value_check
person_6_known person_6_known
person_6_age person_6_age
age_6_net_income_value_check
person_6_lead_partner person_6_lead_partner
no_household_member_likely_to_be_pregnant_person_age_6_check no_household_member_likely_to_be_pregnant_person_age_6_check
age_6_under_retirement_value_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_under_retirement_value_check
working_situation_6_over_retirement_value_check working_situation_6_over_retirement_value_check
working_situation_6_long_term_illness_check working_situation_6_long_term_illness_check
working_situation_6_net_income_value_check
person_7_known person_7_known
person_7_age person_7_age
age_7_net_income_value_check
person_7_lead_partner person_7_lead_partner
no_household_member_likely_to_be_pregnant_person_age_7_check no_household_member_likely_to_be_pregnant_person_age_7_check
age_7_under_retirement_value_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_under_retirement_value_check
working_situation_7_over_retirement_value_check working_situation_7_over_retirement_value_check
working_situation_7_long_term_illness_check working_situation_7_long_term_illness_check
working_situation_7_net_income_value_check
person_8_known person_8_known
person_8_age person_8_age
age_8_net_income_value_check
person_8_lead_partner person_8_lead_partner
no_household_member_likely_to_be_pregnant_person_age_8_check no_household_member_likely_to_be_pregnant_person_age_8_check
age_8_under_retirement_value_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_under_retirement_value_check
working_situation_8_over_retirement_value_check working_situation_8_over_retirement_value_check
working_situation_8_long_term_illness_check working_situation_8_long_term_illness_check
working_situation_8_net_income_value_check
], ],
) )
end 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[ %w[
income_known income_known
income_amount income_amount
net_income_value_check income_amount_net_income_value_check
housing_benefit housing_benefit
benefits_proportion benefits_proportion
rent_or_other_charges rent_or_other_charges
@ -58,7 +58,7 @@ RSpec.describe Form::Lettings::Subsections::IncomeAndBenefits, type: :model do
%w[ %w[
income_known income_known
income_amount income_amount
net_income_value_check income_amount_net_income_value_check
housing_benefit housing_benefit
benefits_proportion benefits_proportion
rent_or_other_charges rent_or_other_charges

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

@ -1,11 +1,13 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::MonthlyRentStaircasingOwned, type: :model do RSpec.describe Form::Sales::Pages::MonthlyRentStaircasingOwned, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil } let(:page_id) { nil }
let(:page_definition) { 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)

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

@ -1,11 +1,13 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::MonthlyRentStaircasing, type: :model do RSpec.describe Form::Sales::Pages::MonthlyRentStaircasing, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil } let(:page_id) { nil }
let(:page_definition) { 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) 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_id) { nil }
let(:page_definition) { 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) 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_id) { "purchase_price" }
let(:page_definition) { nil } 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) 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) } let(:subsection) { instance_double(Form::Subsection) }
before do 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 end
it "has correct subsection" do 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_id) { "value_shared_ownership" }
let(:page_definition) { nil } 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 before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false) 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 it "has the correct question_number" do
expect(question.question_number).to eq(17) expect(question.question_number).to eq(17)
end end
it "has correct guidance partial" do
expect(question.top_guidance_partial).to eq("building_height_class")
end
end end

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

@ -1,11 +1,14 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :model do RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page) } subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { 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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -34,4 +37,8 @@ RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :mode
it "has correct min" do it "has correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(0)
end end
it "has correct max" do
expect(question.max).to eq(9_999)
end
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_id) { nil }
let(:question_definition) { 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 it "has correct page" do
expect(question.page).to be(page) 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) expect(question.min).to be(1)
end end
it "has correct max" do
expect(question.max).to eq(999_999)
end
context "when the mortgage is not used" do context "when the mortgage is not used" do
let(:log) { build(:sales_log, :completed, mortgageused: 2, deposit: nil) } 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(:question_definition) { nil }
let(:start_year) { current_collection_start_year } let(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false } 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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -65,6 +65,10 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
it "has correct min" do it "has correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(0)
end end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end end
context "with year 2026", metadata: { year: 26 } do 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 it "has correct min" do
expect(question.min).to eq(15_000) expect(question.min).to eq(15_000)
end end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end 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(:question_definition) { nil }
let(:start_year) { current_collection_start_year } let(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false } 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")) } 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")) }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -45,6 +41,10 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do
it "has correct min" do it "has correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(0)
end end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end end
context "with year 2026", metadata: { year: 26 } do 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 it "has correct min" do
expect(question.min).to eq(15_000) expect(question.min).to eq(15_000)
end end
it "has correct max" do
expect(question.max).to eq(999_999)
end
end end
end end

12
spec/models/validations/household_validations_spec.rb

@ -252,18 +252,18 @@ RSpec.describe Validations::HouseholdValidations do
record.hhmemb = 0 record.hhmemb = 0
household_validator.validate_numeric_min_max(record) household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"]) 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 end
it "validates that the number of household members cannot be more than 8" do it "validates that the number of household members cannot be more than 15" do
record.hhmemb = 9 record.hhmemb = 16
household_validator.validate_numeric_min_max(record) household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"]) 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 end
it "expects that the number of other household members is between the min and max" do it "expects that the number of household members is between the min and max" do
record.hhmemb = 5 record.hhmemb = 11
household_validator.validate_numeric_min_max(record) household_validator.validate_numeric_min_max(record)
expect(record.errors["hhmemb"]).to be_empty expect(record.errors["hhmemb"]).to be_empty
end end

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

@ -252,12 +252,27 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
context "when initial purchase date == saledate" do 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)) }
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
it "does not add an error" do it "does not add an error" do
sale_information_validator.validate_staircasing_initial_purchase_date(record) sale_information_validator.validate_staircasing_initial_purchase_date(record)
expect(record.errors[:initialpurchase]).not_to be_present 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 end
end end
@ -315,7 +330,10 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
context "when last transaction date == saledate" do 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)) }
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
it "does not add an error" do it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record) sale_information_validator.validate_staircasing_last_transaction_date(record)
@ -324,6 +342,18 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
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
context "when last transaction date after initial purchase date" do context "when last transaction date after initial purchase date" do
let(:record) { build(:sales_log, initialpurchase: 2.months.ago, lasttransaction: 1.month.ago) } let(:record) { build(:sales_log, initialpurchase: 2.months.ago, lasttransaction: 1.month.ago) }
@ -346,7 +376,10 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
context "when last transaction date == initial purchase date" do 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) }
context "and 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
it "does not add an error" do it "does not add an error" do
sale_information_validator.validate_staircasing_last_transaction_date(record) sale_information_validator.validate_staircasing_last_transaction_date(record)
@ -354,6 +387,18 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
expect(record.errors[:lasttransaction]).not_to be_present expect(record.errors[:lasttransaction]).not_to be_present
end end
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 end
describe "#validate_previous_property_unit_type" do describe "#validate_previous_property_unit_type" do

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

@ -646,9 +646,9 @@ RSpec.describe BulkUpload::Lettings::Year2025::RowParser do
end end
describe "invalid fields" do describe "invalid fields" do
context "when a field has been marked as invalid" do
let(:attributes) { setup_section_params.merge({ field_45: 0 }) } let(:attributes) { setup_section_params.merge({ field_45: 0 }) }
context "when a field has been marked as invalid" do
before do before do
parser.add_invalid_field("field_45") parser.add_invalid_field("field_45")
end 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?")) 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
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 end
end end

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

@ -538,9 +538,9 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
end end
describe "invalid fields" do describe "invalid fields" do
context "when a field has been marked as invalid" do
let(:attributes) { setup_section_params.merge({ field_46: 0 }) } let(:attributes) { setup_section_params.merge({ field_46: 0 }) }
context "when a field has been marked as invalid" do
before do before do
parser.add_invalid_field("field_46") parser.add_invalid_field("field_46")
end 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?"))) 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
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
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 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_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 let(:attributes) do
valid_attributes valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase }) .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 end
describe "invalid fields" do describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_31: 0 }) }
context "when a field has been marked as invalid" do 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 before do
parser.add_invalid_field("field_31") parser.add_invalid_field("field_31")
end 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?"))) 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
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 end
end end
@ -1764,6 +1778,14 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
end end
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 describe "#prevtenbuy2" do
let(:attributes) { setup_section_params.merge({ field_64: "R" }) } 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 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_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 let(:attributes) do
valid_attributes valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase }) .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 end
describe "invalid fields" do describe "invalid fields" do
let(:attributes) { setup_section_params.merge({ field_34: 0 }) }
context "when a field has been marked as invalid" do 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 before do
parser.add_invalid_field("field_34") parser.add_invalid_field("field_34")
end 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?"))) 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
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 end
end end
@ -1826,6 +1840,14 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
end end
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 describe "#prevtenbuy2" do
let(:attributes) { setup_section_params.merge({ field_77: "R" }) } let(:attributes) { setup_section_params.merge({ field_77: "R" }) }

Loading…
Cancel
Save