From 8e7e9f1b71962b9f7c16f1fe123979b7783d1833 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Tue, 3 Mar 2026 16:47:13 +0000 Subject: [PATCH] CLDC-4167, CLDC-4168: Add don't know option to mortgage length questions (#3195) * CLDC-4168: Add check for mortgage length known * CLDC-4168: Add mortgage length known to question page * CLDC-4168: Make mortgage length conditional on interview requires a little changing to question logic to account for if the conditional question is not visible right now * CLDC-4168: Add R override to BU * CLDC-4168: Enforce mortlen is not R if buyer not interviewed in BU * CLDC-4168: Update tests * CLDC-4168: Add a lint task that autocorrects * CLDC-4168: Add mortgage length known to sales log factory * CLDC-4168: Allow mortgage not know if buyer wasn't interviewed previously had this the wrong way around. this makes sense because if you interviewed the buyer you should know this * CLDC-4168: Rename mortgage_length_known to mortlen_known matches the mortlen field * fixup! CLDC-4168: Add mortgage length known to question page remove old year numbers * fixup! CLDC-4168: Rename mortgage_length_known to mortlen_known * fixup! CLDC-4168: Allow mortgage not know if buyer wasn't interviewed missed test fix * CLDC-4168: Revert changes to outright_sale section * fixup! CLDC-4168: Revert changes to outright_sale section * fixup! CLDC-4168: Update tests * CLDC-4168: Allow for "r" in mortlen --- .../derived_variables/sales_log_variables.rb | 4 + app/models/form/question.rb | 5 +- .../pages/mortgage_length_interviewed.rb | 15 ++ .../pages/mortgage_length_not_interviewed.rb | 16 ++ .../form/sales/questions/mortgage_length.rb | 1 + .../sales/questions/mortgage_length_known.rb | 21 +++ .../discounted_ownership_scheme.rb | 4 +- .../form/sales/subsections/outright_sale.rb | 2 + .../shared_ownership_initial_purchase.rb | 4 +- app/models/sales_log.rb | 4 + .../bulk_upload/sales/year2026/row_parser.rb | 54 ++++-- .../forms/2026/sales/sale_information.en.yml | 7 + .../validations/sales/2026/bulk_upload.en.yml | 3 + ...4141705_add_mortlen_known_to_sales_logs.rb | 5 + db/schema.rb | 3 +- lib/tasks/lint.rake | 5 + spec/factories/sales_log.rb | 1 + .../discounted_ownership_scheme_spec.rb | 136 +++++++++----- .../shared_ownership_initial_purchase_spec.rb | 173 ++++++++++++++---- .../sales/year2026/row_parser_spec.rb | 122 +++++++++++- 20 files changed, 489 insertions(+), 96 deletions(-) create mode 100644 app/models/form/sales/pages/mortgage_length_interviewed.rb create mode 100644 app/models/form/sales/pages/mortgage_length_not_interviewed.rb create mode 100644 app/models/form/sales/questions/mortgage_length_known.rb create mode 100644 db/migrate/20260224141705_add_mortlen_known_to_sales_logs.rb diff --git a/app/models/derived_variables/sales_log_variables.rb b/app/models/derived_variables/sales_log_variables.rb index a31cfab61..7f160b637 100644 --- a/app/models/derived_variables/sales_log_variables.rb +++ b/app/models/derived_variables/sales_log_variables.rb @@ -98,6 +98,10 @@ module DerivedVariables::SalesLogVariables self.numstair = is_firststair? ? 1 : nil if numstair == 1 && firststair_changed? self.mrent = 0 if stairowned_100? + if buyer_interviewed_changed_to_not_interviewed_and_mortlen_set? + self.mortlen_known = 0 + end + set_encoded_derived_values!(DEPENDENCIES) end diff --git a/app/models/form/question.rb b/app/models/form/question.rb index be90899f9..b3e7a36f7 100644 --- a/app/models/form/question.rb +++ b/app/models/form/question.rb @@ -340,7 +340,10 @@ private end def evaluate_condition(condition, log) - case page.questions.find { |q| q.id == condition[:from] }.type + conditional_question = page.questions.find { |q| q.id == condition[:from] } + return true unless conditional_question + + case conditional_question.type when "numeric" operator = condition[:cond][/[<>=]+/].to_sym operand = condition[:cond][/\d+/].to_i diff --git a/app/models/form/sales/pages/mortgage_length_interviewed.rb b/app/models/form/sales/pages/mortgage_length_interviewed.rb new file mode 100644 index 000000000..f2bebf148 --- /dev/null +++ b/app/models/form/sales/pages/mortgage_length_interviewed.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::MortgageLengthInterviewed < ::Form::Page + def initialize(id, hsh, subsection, ownershipsch:) + super(id, hsh, subsection) + @ownershipsch = ownershipsch + @depends_on = [{ + "mortgageused" => 1, "buyer_not_interviewed?" => false + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::MortgageLength.new(nil, nil, self, ownershipsch: @ownershipsch), + ] + end +end diff --git a/app/models/form/sales/pages/mortgage_length_not_interviewed.rb b/app/models/form/sales/pages/mortgage_length_not_interviewed.rb new file mode 100644 index 000000000..810e904c3 --- /dev/null +++ b/app/models/form/sales/pages/mortgage_length_not_interviewed.rb @@ -0,0 +1,16 @@ +class Form::Sales::Pages::MortgageLengthNotInterviewed < ::Form::Page + def initialize(id, hsh, subsection, ownershipsch:) + super(id, hsh, subsection) + @ownershipsch = ownershipsch + @depends_on = [ + { "mortgageused" => 1, "buyer_not_interviewed?" => true }, + ] + end + + def questions + @questions ||= [ + Form::Sales::Questions::MortgageLengthKnown.new(nil, nil, self, ownershipsch: @ownershipsch), + Form::Sales::Questions::MortgageLength.new(nil, nil, self, ownershipsch: @ownershipsch), + ] + end +end diff --git a/app/models/form/sales/questions/mortgage_length.rb b/app/models/form/sales/questions/mortgage_length.rb index 552b86f56..493deb11d 100644 --- a/app/models/form/sales/questions/mortgage_length.rb +++ b/app/models/form/sales/questions/mortgage_length.rb @@ -19,5 +19,6 @@ class Form::Sales::Questions::MortgageLength < ::Form::Question 2023 => { 1 => 93, 2 => 106, 3 => 114 }, 2024 => { 1 => 94, 2 => 107, 3 => 114 }, 2025 => { 1 => 84, 2 => 108 }, + 2026 => { 1 => 84, 2 => 108 }, }.freeze end diff --git a/app/models/form/sales/questions/mortgage_length_known.rb b/app/models/form/sales/questions/mortgage_length_known.rb new file mode 100644 index 000000000..5d2598771 --- /dev/null +++ b/app/models/form/sales/questions/mortgage_length_known.rb @@ -0,0 +1,21 @@ +class Form::Sales::Questions::MortgageLengthKnown < ::Form::Question + def initialize(id, hsh, page, ownershipsch:) + super(id, hsh, page) + @id = "mortlen_known" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @conditional_for = { "mortlen" => [0] } + @hidden_in_check_answers = { + "depends_on" => [ + { "mortlen_known" => 0 }, + ], + } + @question_number = QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.fetch(form.start_date.year, QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.max_by { |k, _v| k }.last)[ownershipsch] + end + + ANSWER_OPTIONS = { "0" => { "value" => "Yes" }, "1" => { "value" => "No" } }.freeze + + QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP = { + 2026 => { 1 => 84, 2 => 108 }, + }.freeze +end diff --git a/app/models/form/sales/subsections/discounted_ownership_scheme.rb b/app/models/form/sales/subsections/discounted_ownership_scheme.rb index c74dcd262..dea8334b8 100644 --- a/app/models/form/sales/subsections/discounted_ownership_scheme.rb +++ b/app/models/form/sales/subsections/discounted_ownership_scheme.rb @@ -30,7 +30,9 @@ class Form::Sales::Subsections::DiscountedOwnershipScheme < ::Form::Subsection Form::Sales::Pages::ExtraBorrowingValueCheck.new("extra_borrowing_mortgage_value_check", nil, self), Form::Sales::Pages::DepositAndMortgageValueCheck.new("discounted_ownership_deposit_and_mortgage_value_check_after_mortgage", nil, self), mortgage_lender_questions, - Form::Sales::Pages::MortgageLength.new("mortgage_length_discounted_ownership", nil, self, ownershipsch: 2), + (Form::Sales::Pages::MortgageLength.new("mortgage_length_discounted_ownership", nil, self, ownershipsch: 2) unless form.start_year_2026_or_later?), + (Form::Sales::Pages::MortgageLengthNotInterviewed.new("mortgage_length_discounted_ownership_not_interviewed", nil, self, ownershipsch: 2) if form.start_year_2026_or_later?), + (Form::Sales::Pages::MortgageLengthInterviewed.new("mortgage_length_discounted_ownership_interviewed", nil, self, ownershipsch: 2) if form.start_year_2026_or_later?), Form::Sales::Pages::ExtraBorrowing.new("extra_borrowing_discounted_ownership", nil, self, ownershipsch: 2), Form::Sales::Pages::ExtraBorrowingValueCheck.new("extra_borrowing_value_check", nil, self), Form::Sales::Pages::Deposit.new("deposit_discounted_ownership", nil, self, ownershipsch: 2, optional: false), diff --git a/app/models/form/sales/subsections/outright_sale.rb b/app/models/form/sales/subsections/outright_sale.rb index afa0f4a69..0a7b2bbea 100644 --- a/app/models/form/sales/subsections/outright_sale.rb +++ b/app/models/form/sales/subsections/outright_sale.rb @@ -1,3 +1,5 @@ +# NOTE: ownershipsch of 3 was last possible in 2024 +# for 2025 logs and later this section is not used class Form::Sales::Subsections::OutrightSale < ::Form::Subsection def initialize(id, hsh, section) super diff --git a/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb index 4bfb10f2d..7ac9c0543 100644 --- a/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb +++ b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb @@ -28,7 +28,9 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect Form::Sales::Pages::MortgageAmount.new("mortgage_amount_shared_ownership", nil, self, ownershipsch: 1), Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_mortgage_amount_value_check", nil, self), Form::Sales::Pages::MortgageValueCheck.new("mortgage_amount_mortgage_value_check", nil, self), - Form::Sales::Pages::MortgageLength.new("mortgage_length_shared_ownership", nil, self, ownershipsch: 1), + (Form::Sales::Pages::MortgageLength.new("mortgage_length_shared_ownership", nil, self, ownershipsch: 1) unless form.start_year_2026_or_later?), + (Form::Sales::Pages::MortgageLengthNotInterviewed.new("mortgage_length_shared_ownership_not_interviewed", nil, self, ownershipsch: 1) if form.start_year_2026_or_later?), + (Form::Sales::Pages::MortgageLengthInterviewed.new("mortgage_length_shared_ownership_interviewed", nil, self, ownershipsch: 1) if form.start_year_2026_or_later?), Form::Sales::Pages::Deposit.new("deposit_shared_ownership", nil, self, ownershipsch: 1, optional: false), Form::Sales::Pages::Deposit.new("deposit_shared_ownership_optional", nil, self, ownershipsch: 1, optional: true), Form::Sales::Pages::DepositValueCheck.new("deposit_joint_purchase_value_check", nil, self, joint_purchase: true), diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index cf9e0dd44..654808d3a 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -390,6 +390,10 @@ class SalesLog < Log proptype_changed? && proptype_was == 2 end + def buyer_interviewed_changed_to_not_interviewed_and_mortlen_set? + noint_changed? && noint_was == 2 && buyer_not_interviewed? && mortlen.present? + end + def shared_ownership_scheme? ownershipsch == 1 end diff --git a/app/services/bulk_upload/sales/year2026/row_parser.rb b/app/services/bulk_upload/sales/year2026/row_parser.rb index 2274a45db..35f412a7a 100644 --- a/app/services/bulk_upload/sales/year2026/row_parser.rb +++ b/app/services/bulk_upload/sales/year2026/row_parser.rb @@ -161,6 +161,9 @@ class BulkUpload::Sales::Year2026::RowParser :field_75, # What is the total amount the buyers had in savings before they paid any deposit for the property? :field_70, # What is buyer 1’s gross annual income? :field_72, # What is buyer 2’s gross annual income? + + :field_90, # What is the length of the mortgage in years? - Shared ownership + :field_118, # What is the length of the mortgage in years? - Discounted ownership ].freeze attribute :bulk_upload @@ -264,7 +267,7 @@ class BulkUpload::Sales::Year2026::RowParser attribute :field_87, :decimal attribute :field_88, :integer attribute :field_89, :decimal - attribute :field_90, :integer + attribute :field_90, :string attribute :field_91, :decimal attribute :field_92, :decimal attribute :field_93, :decimal @@ -294,7 +297,7 @@ class BulkUpload::Sales::Year2026::RowParser attribute :field_115, :decimal attribute :field_116, :integer attribute :field_117, :decimal - attribute :field_118, :integer + attribute :field_118, :string attribute :field_119, :integer attribute :field_120, :decimal attribute :field_121, :decimal @@ -427,6 +430,22 @@ class BulkUpload::Sales::Year2026::RowParser }, on: :before_log + validates :field_90, + if: :shared_ownership?, + format: { + with: /\A(\d+|R)\z/, + message: I18n.t("#{ERROR_BASE_KEY}.mortlen.invalid"), + }, + on: :after_log + + validates :field_118, + if: :discounted_ownership?, + format: { + with: /\A(\d+|R)\z/, + message: I18n.t("#{ERROR_BASE_KEY}.mortlen.invalid"), + }, + on: :after_log + validate :validate_buyer1_economic_status, on: :before_log validate :validate_buyer2_economic_status, on: :before_log validate :validate_valid_radio_option, on: :before_log @@ -449,6 +468,7 @@ class BulkUpload::Sales::Year2026::RowParser validate :validate_nationality, on: :after_log validate :validate_buyer_2_nationality, on: :after_log + validate :validate_mortlen_field_if_buyer_interviewed, on: :after_log validate :validate_nulls, on: :after_log @@ -674,6 +694,10 @@ private field_99 == 1 end + def buyer_interviewed? + field_14 == 2 + end + def rtb_like_sale_type? [9, 14, 27, 29].include?(field_11) end @@ -767,6 +791,7 @@ private hb: %i[field_74], mortlen: mortlen_fields, + mortlen_known: mortlen_fields, proplen: proplen_fields, jointmore: %i[field_13], @@ -938,7 +963,8 @@ private attributes["hb"] = field_74 - attributes["mortlen"] = mortlen + attributes["mortlen"] = mortlen != "R" ? mortlen : nil + attributes["mortlen_known"] = mortlen_known attributes["proplen"] = proplen if proplen&.positive? attributes["proplen_asked"] = attributes["proplen"].present? ? 0 : 1 @@ -1163,6 +1189,16 @@ private field_118 if discounted_ownership? end + def mortlen_known + return nil if buyer_interviewed? + + if mortlen == "R" + 1 + else + 0 + end + end + def proplen return field_79 if shared_ownership? @@ -1528,14 +1564,10 @@ private %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say" end - def validate_relat_fields - %i[field_34 field_42 field_46 field_50 field_54].each do |field| - value = send(field) - next if value.blank? - - unless (1..3).cover?(value) - errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field]))) - end + def validate_mortlen_field_if_buyer_interviewed + if buyer_interviewed? && mortlen == "R" + errors.add(:field_90, I18n.t("#{ERROR_BASE_KEY}.mortlen.invalid_for_interviewed")) if shared_ownership? + errors.add(:field_118, I18n.t("#{ERROR_BASE_KEY}.mortlen.invalid_for_interviewed")) if discounted_ownership? end end diff --git a/config/locales/forms/2026/sales/sale_information.en.yml b/config/locales/forms/2026/sales/sale_information.en.yml index 1d71145e6..b253e4074 100644 --- a/config/locales/forms/2026/sales/sale_information.en.yml +++ b/config/locales/forms/2026/sales/sale_information.en.yml @@ -204,6 +204,13 @@ en: hint_text: "You should round up to the nearest year. Value should not exceed 60 years." question_text: "What is the length of the mortgage?" + mortlen_known: + page_header: "" + check_answer_label: "Length of mortgage known" + check_answer_prompt: "" + hint_text: "" + question_text: "Do you know the length of the mortgage?" + extrabor: page_header: "" check_answer_label: "Any other borrowing" diff --git a/config/locales/validations/sales/2026/bulk_upload.en.yml b/config/locales/validations/sales/2026/bulk_upload.en.yml index c5c96818a..a45ade4ea 100644 --- a/config/locales/validations/sales/2026/bulk_upload.en.yml +++ b/config/locales/validations/sales/2026/bulk_upload.en.yml @@ -44,3 +44,6 @@ en: not_answered: "Enter either the UPRN or the full address." nationality: invalid: "Select a valid nationality." + mortlen: + invalid: "Mortgage length must be a number or the letter R" + invalid_for_interviewed: "You indicated that the buyer was interviewed, but selected “Don’t know” for mortgage length. Please provide the mortgage length or update your response." diff --git a/db/migrate/20260224141705_add_mortlen_known_to_sales_logs.rb b/db/migrate/20260224141705_add_mortlen_known_to_sales_logs.rb new file mode 100644 index 000000000..d9f4ed2f4 --- /dev/null +++ b/db/migrate/20260224141705_add_mortlen_known_to_sales_logs.rb @@ -0,0 +1,5 @@ +class AddMortlenKnownToSalesLogs < ActiveRecord::Migration[7.2] + def change + add_column :sales_logs, :mortlen_known, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index dfde8e765..a32fd7ab4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_02_25_162121) do +ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -824,6 +824,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_25_162121) do t.string "sexrab4" t.string "sexrab5" t.string "sexrab6" + t.integer "mortlen_known" t.integer "buildheightclass" t.index ["assigned_to_id"], name: "index_sales_logs_on_assigned_to_id" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 5ace4b11d..b304d9b67 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -3,6 +3,11 @@ task rubocop: :environment do sh "bundle exec rubocop" end +desc "Run Rubocop Autocorrect" +task rubocop_autocorrect: :environment do + sh "bundle exec rubocop -A" +end + desc "Run ERB Lint" task erblint: :environment do sh "bundle exec erb_lint --lint-all" diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index 2cdc688be..747bb89af 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -167,6 +167,7 @@ FactoryBot.define do has_mscharge { 1 } mscharge { 100 } mortlen { 10 } + mortlen_known { 0 } pcodenk { 0 } postcode_full { "SW1A 1AA" } is_la_inferred { false } diff --git a/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb b/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb index 1420564ae..dc91a8f92 100644 --- a/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb +++ b/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb @@ -1,57 +1,21 @@ require "rails_helper" RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model do + include CollectionTimeHelper + subject(:discounted_ownership_scheme) { described_class.new(subsection_id, subsection_definition, section) } let(:subsection_id) { nil } let(:subsection_definition) { nil } - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2025_or_later?: false) } + let(:start_year_2025_or_later?) { true } + let(:start_year_2026_or_later?) { true } + let(:form) { instance_double(Form, start_date: current_collection_start_date, start_year_2025_or_later?: start_year_2025_or_later?, start_year_2026_or_later?: start_year_2026_or_later?) } let(:section) { instance_double(Form::Sales::Sections::SaleInformation, form:) } it "has correct section" do expect(discounted_ownership_scheme.section).to eq(section) end - it "has correct pages" do - expect(discounted_ownership_scheme.pages.map(&:id)).to eq( - %w[ - living_before_purchase_discounted_ownership_joint_purchase - living_before_purchase_discounted_ownership - purchase_price - discount - extra_borrowing_price_value_check - percentage_discount_value_check - grant - grant_value_check - purchase_price_discounted_ownership - discounted_sale_grant_value_check - about_price_discounted_ownership_value_check - discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount - mortgage_used_discounted_ownership - discounted_ownership_mortgage_used_mortgage_value_check - discounted_sale_mortgage_used_value_check - mortgage_amount_discounted_ownership - discounted_ownership_mortgage_amount_mortgage_value_check - discounted_sale_mortgage_value_check - extra_borrowing_mortgage_value_check - discounted_ownership_deposit_and_mortgage_value_check_after_mortgage - mortgage_lender_discounted_ownership - mortgage_lender_other_discounted_ownership - mortgage_length_discounted_ownership - extra_borrowing_discounted_ownership - extra_borrowing_value_check - deposit_discounted_ownership - extra_borrowing_deposit_value_check - discounted_ownership_deposit_joint_purchase_value_check - discounted_ownership_deposit_value_check - discounted_ownership_deposit_and_mortgage_value_check_after_deposit - discounted_sale_deposit_value_check - leasehold_charges_discounted_ownership - monthly_charges_discounted_ownership_value_check - ], - ) - end - it "has the correct id" do expect(discounted_ownership_scheme.id).to eq("discounted_ownership_scheme") end @@ -88,8 +52,9 @@ RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model end end - context "with form on or after 2025" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2025, 4, 1), start_year_2025_or_later?: true) } + context "when 2024", metadata: { year: 24 } do + let(:start_year_2025_or_later?) { false } + let(:start_year_2026_or_later?) { false } it "has correct pages" do expect(discounted_ownership_scheme.pages.map(&:id)).to eq( @@ -114,6 +79,8 @@ RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model discounted_sale_mortgage_value_check extra_borrowing_mortgage_value_check discounted_ownership_deposit_and_mortgage_value_check_after_mortgage + mortgage_lender_discounted_ownership + mortgage_lender_other_discounted_ownership mortgage_length_discounted_ownership extra_borrowing_discounted_ownership extra_borrowing_value_check @@ -129,4 +96,87 @@ RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model ) end end + + context "when 2025", metadata: { year: 25 } do + let(:start_year_2026_or_later?) { false } + + it "has correct pages" do + expect(discounted_ownership_scheme.pages.map(&:id)).to eq( + %w[ + living_before_purchase_discounted_ownership_joint_purchase + living_before_purchase_discounted_ownership + purchase_price + discount + extra_borrowing_price_value_check + percentage_discount_value_check + grant + grant_value_check + purchase_price_discounted_ownership + discounted_sale_grant_value_check + about_price_discounted_ownership_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount + mortgage_used_discounted_ownership + discounted_ownership_mortgage_used_mortgage_value_check + discounted_sale_mortgage_used_value_check + mortgage_amount_discounted_ownership + discounted_ownership_mortgage_amount_mortgage_value_check + discounted_sale_mortgage_value_check + extra_borrowing_mortgage_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_mortgage + mortgage_length_discounted_ownership + extra_borrowing_discounted_ownership + extra_borrowing_value_check + deposit_discounted_ownership + extra_borrowing_deposit_value_check + discounted_ownership_deposit_joint_purchase_value_check + discounted_ownership_deposit_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_deposit + discounted_sale_deposit_value_check + leasehold_charges_discounted_ownership + monthly_charges_discounted_ownership_value_check + ], + ) + end + end + + context "when 2026", metadata: { year: 26 } do + it "has correct pages" do + expect(discounted_ownership_scheme.pages.map(&:id)).to eq( + %w[ + living_before_purchase_discounted_ownership_joint_purchase + living_before_purchase_discounted_ownership + purchase_price + discount + extra_borrowing_price_value_check + percentage_discount_value_check + grant + grant_value_check + purchase_price_discounted_ownership + discounted_sale_grant_value_check + about_price_discounted_ownership_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount + mortgage_used_discounted_ownership + discounted_ownership_mortgage_used_mortgage_value_check + discounted_sale_mortgage_used_value_check + mortgage_amount_discounted_ownership + discounted_ownership_mortgage_amount_mortgage_value_check + discounted_sale_mortgage_value_check + extra_borrowing_mortgage_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_mortgage + mortgage_length_discounted_ownership_not_interviewed + mortgage_length_discounted_ownership_interviewed + extra_borrowing_discounted_ownership + extra_borrowing_value_check + deposit_discounted_ownership + extra_borrowing_deposit_value_check + discounted_ownership_deposit_joint_purchase_value_check + discounted_ownership_deposit_value_check + discounted_ownership_deposit_and_mortgage_value_check_after_deposit + discounted_sale_deposit_value_check + leasehold_charges_discounted_ownership + monthly_charges_discounted_ownership_value_check + ], + ) + end + end end diff --git a/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb b/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb index e60ff58dd..984247af6 100644 --- a/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb +++ b/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb @@ -1,56 +1,155 @@ require "rails_helper" RSpec.describe Form::Sales::Subsections::SharedOwnershipInitialPurchase, type: :model do + include CollectionTimeHelper + subject(:shared_ownership_initial_purchase) { described_class.new(subsection_id, subsection_definition, section) } let(:subsection_id) { nil } let(:subsection_definition) { nil } + let(:start_year_2024_or_later?) { true } + let(:start_year_2025_or_later?) { true } + let(:start_year_2026_or_later?) { true } + let(:start_date) { current_collection_start_date } + let(:form) { instance_double(Form, start_date:, start_year_2024_or_later?: start_year_2024_or_later?, start_year_2025_or_later?: start_year_2025_or_later?, start_year_2026_or_later?: start_year_2026_or_later?) } let(:section) { instance_double(Form::Sales::Sections::SaleInformation) } before do - allow(section).to receive(:form).and_return(instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) + allow(section).to receive(:form).and_return(form) end it "has correct section" do expect(shared_ownership_initial_purchase.section).to eq(section) end - it "has correct pages" do - expect(shared_ownership_initial_purchase.pages.map(&:id)).to eq( - %w[ - resale - living_before_purchase_shared_ownership_joint_purchase - living_before_purchase_shared_ownership - handover_date - handover_date_check - buyer_previous_joint_purchase - buyer_previous_not_joint_purchase - previous_bedrooms - previous_property_type - shared_ownership_previous_tenure - value_shared_ownership - about_price_shared_ownership_value_check - initial_equity - shared_ownership_equity_value_check - mortgage_used_shared_ownership - mortgage_used_mortgage_value_check - mortgage_amount_shared_ownership - shared_ownership_mortgage_amount_value_check - mortgage_amount_mortgage_value_check - mortgage_length_shared_ownership - deposit_shared_ownership - deposit_shared_ownership_optional - deposit_joint_purchase_value_check - deposit_value_check - deposit_discount - deposit_discount_optional - shared_ownership_deposit_value_check - monthly_rent - service_charge - monthly_charges_shared_ownership_value_check - estate_management_fee - ], - ) + context "when 2024", metadata: { year: 24 } do + let(:start_year_2025_or_later?) { false } + let(:start_year_2026_or_later?) { false } + let(:start_date) { collection_start_date_for_year(2024) } + + it "has correct pages" do + expect(shared_ownership_initial_purchase.pages.map(&:id)).to eq( + %w[ + resale + living_before_purchase_shared_ownership_joint_purchase + living_before_purchase_shared_ownership + handover_date + handover_date_check + buyer_previous_joint_purchase + buyer_previous_not_joint_purchase + previous_bedrooms + previous_property_type + shared_ownership_previous_tenure + value_shared_ownership + about_price_shared_ownership_value_check + initial_equity + shared_ownership_equity_value_check + mortgage_used_shared_ownership + mortgage_used_mortgage_value_check + mortgage_amount_shared_ownership + shared_ownership_mortgage_amount_value_check + mortgage_amount_mortgage_value_check + mortgage_length_shared_ownership + deposit_shared_ownership + deposit_shared_ownership_optional + deposit_joint_purchase_value_check + deposit_value_check + deposit_discount + deposit_discount_optional + shared_ownership_deposit_value_check + monthly_rent + service_charge + monthly_charges_shared_ownership_value_check + estate_management_fee + ], + ) + end + end + + context "when 2025", metadata: { year: 25 } do + let(:start_year_2026_or_later?) { false } + let(:start_date) { collection_start_date_for_year(2025) } + + it "has correct pages" do + expect(shared_ownership_initial_purchase.pages.map(&:id)).to eq( + %w[ + resale + living_before_purchase_shared_ownership_joint_purchase + living_before_purchase_shared_ownership + handover_date + handover_date_check + buyer_previous_joint_purchase + buyer_previous_not_joint_purchase + previous_bedrooms + previous_property_type + shared_ownership_previous_tenure + value_shared_ownership + about_price_shared_ownership_value_check + initial_equity + shared_ownership_equity_value_check + mortgage_used_shared_ownership + mortgage_used_mortgage_value_check + mortgage_amount_shared_ownership + shared_ownership_mortgage_amount_value_check + mortgage_amount_mortgage_value_check + mortgage_length_shared_ownership + deposit_shared_ownership + deposit_shared_ownership_optional + deposit_joint_purchase_value_check + deposit_value_check + deposit_discount + deposit_discount_optional + shared_ownership_deposit_value_check + monthly_rent + service_charge + monthly_charges_shared_ownership_value_check + estate_management_fee + ], + ) + end + end + + context "when 2026", metadata: { year: 26 } do + let(:start_date) { collection_start_date_for_year(2026) } + + it "has correct pages" do + expect(shared_ownership_initial_purchase.pages.map(&:id)).to eq( + %w[ + resale + living_before_purchase_shared_ownership_joint_purchase + living_before_purchase_shared_ownership + handover_date + handover_date_check + buyer_previous_joint_purchase + buyer_previous_not_joint_purchase + previous_bedrooms + previous_property_type + shared_ownership_previous_tenure + value_shared_ownership + about_price_shared_ownership_value_check + initial_equity + shared_ownership_equity_value_check + mortgage_used_shared_ownership + mortgage_used_mortgage_value_check + mortgage_amount_shared_ownership + shared_ownership_mortgage_amount_value_check + mortgage_amount_mortgage_value_check + mortgage_length_shared_ownership_not_interviewed + mortgage_length_shared_ownership_interviewed + deposit_shared_ownership + deposit_shared_ownership_optional + deposit_joint_purchase_value_check + deposit_value_check + deposit_discount + deposit_discount_optional + shared_ownership_deposit_value_check + monthly_rent + service_charge + monthly_charges_shared_ownership_value_check + estate_management_fee + ], + ) + end end it "has the correct id" do diff --git a/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb index 3547eb240..e34559d92 100644 --- a/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb @@ -296,7 +296,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do context "and case insensitive fields are set to lowercase" do let(:case_insensitive_fields) { %w[field_29 field_36 field_44 field_48 field_52 field_56] } - let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72] } + let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72 field_90 field_118] } let(:attributes) do valid_attributes .merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase }) @@ -1454,6 +1454,74 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do expect(parser.log_already_exists?).to be(false) end end + + describe "field_90" do + context "when field_90 is a number" do + let(:field_90_number_attributes) { valid_attributes.merge({ field_90: 20 }) } + + context "and buyer was interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 2 }) } + + it "does not add an error" do + parser.valid? + expect(parser.errors.where(:field_90)).not_to be_present + end + end + + context "and buyer was not interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 1 }) } + + it "does not add an error" do + parser.valid? + expect(parser.errors.where(:field_90)).not_to be_present + end + end + end + + context "when field_90 is R" do + let(:field_90_number_attributes) { valid_attributes.merge({ field_90: "R" }) } + + context "and buyer was interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 2 }) } + + it "adds an error" do + parser.valid? + expect(parser.errors.where(:field_90)).to be_present + end + end + + context "and buyer was not interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 1 }) } + + it "does not add an error" do + parser.valid? + expect(parser.errors.where(:field_90)).not_to be_present + end + end + end + + context "when field_90 is neither a number nor R" do + let(:field_90_number_attributes) { valid_attributes.merge({ field_90: "something" }) } + + context "and buyer was interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 2 }) } + + it "adds an error" do + parser.valid? + expect(parser.errors.where(:field_90)).to be_present + end + end + + context "and buyer was not interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 1 }) } + + it "adds an error" do + parser.valid? + expect(parser.errors.where(:field_90)).to be_present + end + end + end + end end describe "#log" do @@ -1959,6 +2027,58 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do end end end + + describe "mortlen amd mortlen_known" do + context "when field_90 is a number" do + let(:field_90_number_attributes) { valid_attributes.merge({ field_90: 20 }) } + + context "and buyer was interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 2 }) } + + it "sets mortlen to the length" do + log = parser.log + expect(log.mortlen).to eq(20) + end + + it "sets mortlen_known to nil" do + log = parser.log + expect(log.mortlen_known).to be_nil + end + end + + context "and buyer was not interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 1 }) } + + it "sets mortlen to the length" do + log = parser.log + expect(log.mortlen).to eq(20) + end + + it "sets mortlen_known to yes" do + log = parser.log + expect(log.mortlen_known).to eq(0) + end + end + end + + context "when field_90 is R" do + let(:field_90_number_attributes) { valid_attributes.merge({ field_90: "R" }) } + + context "and buyer was not interviewed" do + let(:attributes) { field_90_number_attributes.merge({ field_14: 1 }) } + + it "sets mortlen to nil" do + log = parser.log + expect(log.mortlen).to be_nil + end + + it "sets mortlen_known to no" do + log = parser.log + expect(log.mortlen_known).to eq(1) + end + end + end + end end describe "#owning_organisation_id" do