diff --git a/Dockerfile b/Dockerfile index 74faebfd8..814582011 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN apk add --update --no-cache tzdata && \ # build-base: compilation tools for bundle # yarn: node package manager # postgresql-dev: postgres driver and libraries -RUN apk add --no-cache build-base=0.5-r3 busybox=1.36.1-r7 nodejs-current=20.8.1-r0 yarn=1.22.19-r0 postgresql13-dev=13.17-r0 git=2.40.3-r0 bash=5.2.15-r5 +RUN apk add --no-cache build-base=0.5-r3 busybox=1.36.1-r7 nodejs-current=20.8.1-r0 yarn=1.22.19-r0 postgresql13-dev=13.18-r0 git=2.40.3-r0 bash=5.2.15-r5 # Bundler version should be the same version as what the Gemfile.lock was bundled with RUN gem install bundler:2.3.14 --no-document diff --git a/Gemfile.lock b/Gemfile.lock index 1a98463b5..d2a5de05d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -246,7 +246,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -274,11 +274,11 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) - nokogiri (1.16.7-arm64-darwin) + nokogiri (1.16.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) + nokogiri (1.16.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.16.8-x86_64-linux) racc (~> 1.4) notifications-ruby-client (6.0.0) jwt (>= 1.5, < 3) @@ -345,9 +345,9 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails_admin (3.1.3) activemodel-serializers-xml (>= 1.0) kaminari (>= 0.14, < 2.0) diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 70b6f892b..7ce63e609 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -5,6 +5,7 @@ class FormController < ApplicationController before_action :find_resource, only: %i[review] before_action :find_resource_by_named_id, except: %i[review] before_action :check_collection_period, only: %i[submit_form show_page] + before_action :set_cache_headers, only: [:show_page] def submit_form if @log @@ -37,8 +38,9 @@ class FormController < ApplicationController error_attributes = @log.errors.map(&:attribute) Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}" @subsection = form.subsection_for_page(@page) - restore_error_field_values(@page&.questions) - render "form/page" + flash[:errors] = @log.errors + flash[:log_data] = responses_for_page + redirect_to send("#{@log.class.name.underscore}_#{@page.id}_path", @log, { referrer: request.params["referrer"], original_page_id: request.params["original_page_id"], related_question_ids: request.params["related_question_ids"] }) end else render_not_found @@ -84,6 +86,10 @@ class FormController < ApplicationController @questions = request.params["related_question_ids"].map { |id| @log.form.get_question(id, @log) } render "form/check_errors" else + if flash[:errors].present? + restore_previous_errors(flash[:errors]) + restore_error_field_values(flash[:log_data]) + end render "form/page" end else @@ -96,13 +102,21 @@ class FormController < ApplicationController private - def restore_error_field_values(questions) - return unless questions + def restore_error_field_values(previous_responses) + return unless previous_responses - questions.each do |question| - if question&.type == "date" && @log.attributes.key?(question.id) - @log[question.id] = @log.send("#{question.id}_was") - end + previous_responses_to_reset = previous_responses.reject do |key, _| + @log.form.get_question(key, @log)&.type == "date" + end + + @log.assign_attributes(previous_responses_to_reset) + end + + def restore_previous_errors(previous_errors) + return unless previous_errors + + previous_errors.each do |attribute, message| + @log.errors.add attribute, message.first end end @@ -431,4 +445,10 @@ private def updated_answer_from_check_errors_page? params["check_errors"] end + + def set_cache_headers + response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Mon, 01 Jan 1990 00:00:00 GMT" + end end diff --git a/app/helpers/tag_helper.rb b/app/helpers/tag_helper.rb index bc0d8e06b..398f897cf 100644 --- a/app/helpers/tag_helper.rb +++ b/app/helpers/tag_helper.rb @@ -30,8 +30,7 @@ module TagHelper }.freeze COLOUR = { - not_started: "grey", - cannot_start_yet: "grey", + not_started: "light-blue", in_progress: "blue", completed: "green", active: "green", @@ -58,6 +57,8 @@ module TagHelper }.freeze def status_tag(status, classes = []) + return nil if COLOUR[status.to_sym].nil? + govuk_tag( classes:, colour: COLOUR[status.to_sym], @@ -65,6 +66,10 @@ module TagHelper ) end + def status_text(status) + TEXT[status.to_sym] + end + def status_tag_from_resource(resource, classes = []) status = resource.status status = :active if resource.deactivates_in_a_long_time? diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index 2caef019a..3a07a00f3 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -41,12 +41,19 @@ module TasklistHelper def subsection_link(subsection, log, current_user) if subsection.status(log) != :cannot_start_yet next_page_path = next_page_or_check_answers(subsection, log, current_user).to_s - govuk_link_to(subsection.label, next_page_path.dasherize, aria: { describedby: subsection.id.dasherize }) + govuk_link_to(subsection.label, next_page_path.dasherize, class: "govuk-task-list__link", aria: { describedby: subsection.id.dasherize }) else subsection.label end end + def subsection_href(subsection, log, current_user) + if subsection.status(log) != :cannot_start_yet + next_page_path = next_page_or_check_answers(subsection, log, current_user).to_s + next_page_path.dasherize + end + end + def review_log_text(log) if log.collection_period_open? path = log.sales? ? review_sales_log_path(id: log, sales_log: true) : review_lettings_log_path(log) @@ -59,6 +66,10 @@ module TasklistHelper end end + def tasklist_link_class(status) + status == :cannot_start_yet ? "" : "govuk-task-list__item--with-link" + end + private def breadcrumb_organisation(log) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 4692a0e6b..ced530b17 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -84,6 +84,15 @@ module DerivedVariables::LettingsLogVariables if uprn_known&.zero? self.uprn = nil + if uprn_known_was == 1 + self.address_line1 = nil + self.address_line2 = nil + self.town_or_city = nil + self.county = nil + self.postcode_known = nil + self.postcode_full = nil + self.la = nil + end end if uprn_known == 1 && uprn_confirmed&.zero? diff --git a/app/models/derived_variables/sales_log_variables.rb b/app/models/derived_variables/sales_log_variables.rb index 0c13d4fdf..6e12ec488 100644 --- a/app/models/derived_variables/sales_log_variables.rb +++ b/app/models/derived_variables/sales_log_variables.rb @@ -53,6 +53,15 @@ module DerivedVariables::SalesLogVariables if uprn_known&.zero? self.uprn = nil + if uprn_known_was == 1 + self.address_line1 = nil + self.address_line2 = nil + self.town_or_city = nil + self.county = nil + self.pcodenk = nil + self.postcode_full = nil + self.la = nil + end end if uprn_known == 1 && uprn_confirmed&.zero? @@ -80,6 +89,9 @@ module DerivedVariables::SalesLogVariables self.is_la_inferred = false end + self.numstair = is_firststair? ? 1 : nil if numstair == 1 && firststair_changed? + self.mrent = 0 if stairowned_100? + set_encoded_derived_values!(DEPENDENCIES) end diff --git a/app/models/form/sales/pages/about_staircase.rb b/app/models/form/sales/pages/about_staircase.rb index d736bae15..2d42c7456 100644 --- a/app/models/form/sales/pages/about_staircase.rb +++ b/app/models/form/sales/pages/about_staircase.rb @@ -18,7 +18,7 @@ class Form::Sales::Pages::AboutStaircase < ::Form::Page end def staircase_sale_question - if form.start_date.year >= 2023 + if [2023, 2024].include?(form.start_date.year) Form::Sales::Questions::StaircaseSale.new(nil, nil, self) end end diff --git a/app/models/form/sales/pages/equity.rb b/app/models/form/sales/pages/equity.rb index 46eec40a3..12d3c0a1b 100644 --- a/app/models/form/sales/pages/equity.rb +++ b/app/models/form/sales/pages/equity.rb @@ -1,7 +1,7 @@ class Form::Sales::Pages::Equity < ::Form::Page def initialize(id, hsh, subsection) super - @id = "equity" + @copy_key = "sales.sale_information.equity" end def questions diff --git a/app/models/form/sales/pages/monthly_rent_staircasing.rb b/app/models/form/sales/pages/monthly_rent_staircasing.rb new file mode 100644 index 000000000..062439c52 --- /dev/null +++ b/app/models/form/sales/pages/monthly_rent_staircasing.rb @@ -0,0 +1,17 @@ +class Form::Sales::Pages::MonthlyRentStaircasing < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "monthly_rent_staircasing" + @copy_key = "sales.sale_information.mrent_staircasing" + @depends_on = [{ + "stairowned_100?" => false, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::MonthlyRentBeforeStaircasing.new(nil, nil, self), + Form::Sales::Questions::MonthlyRentAfterStaircasing.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/monthly_rent_staircasing_owned.rb b/app/models/form/sales/pages/monthly_rent_staircasing_owned.rb new file mode 100644 index 000000000..b772d129f --- /dev/null +++ b/app/models/form/sales/pages/monthly_rent_staircasing_owned.rb @@ -0,0 +1,17 @@ +class Form::Sales::Pages::MonthlyRentStaircasingOwned < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "monthly_rent_staircasing_owned" + @copy_key = "sales.sale_information.mrent_staircasing" + @header = "" + @depends_on = [{ + "stairowned_100?" => true, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::MonthlyRentBeforeStaircasing.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/staircase_first_time.rb b/app/models/form/sales/pages/staircase_first_time.rb new file mode 100644 index 000000000..239a2a930 --- /dev/null +++ b/app/models/form/sales/pages/staircase_first_time.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::StaircaseFirstTime < ::Form::Page + def initialize(id, hsh, subsection) + super(id, hsh, subsection) + @id = "staircase_first_time" + @depends_on = [{ + "staircase" => 1, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::StaircaseFirstTime.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/staircase_initial_date.rb b/app/models/form/sales/pages/staircase_initial_date.rb new file mode 100644 index 000000000..9404440f4 --- /dev/null +++ b/app/models/form/sales/pages/staircase_initial_date.rb @@ -0,0 +1,16 @@ +class Form::Sales::Pages::StaircaseInitialDate < ::Form::Page + def initialize(id, hsh, subsection) + super(id, hsh, subsection) + @id = "staircase_initial_date" + @header = "" + @depends_on = [{ + "is_firststair?" => true, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::StaircaseInitialDate.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/staircase_previous.rb b/app/models/form/sales/pages/staircase_previous.rb new file mode 100644 index 000000000..30d0139ab --- /dev/null +++ b/app/models/form/sales/pages/staircase_previous.rb @@ -0,0 +1,18 @@ +class Form::Sales::Pages::StaircasePrevious < ::Form::Page + def initialize(id, hsh, subsection) + super(id, hsh, subsection) + @id = "staircase_previous" + @copy_key = "sales.sale_information.stairprevious" + @depends_on = [{ + "is_firststair?" => false, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::StaircaseCount.new(nil, nil, self), + Form::Sales::Questions::StaircaseLastDate.new(nil, nil, self), + Form::Sales::Questions::StaircaseInitialDate.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/staircase_sale.rb b/app/models/form/sales/pages/staircase_sale.rb new file mode 100644 index 000000000..116db72b5 --- /dev/null +++ b/app/models/form/sales/pages/staircase_sale.rb @@ -0,0 +1,17 @@ +class Form::Sales::Pages::StaircaseSale < ::Form::Page + def initialize(id, hsh, subsection) + super(id, hsh, subsection) + @id = "staircase_sale" + @copy_key = form.start_year_2025_or_later? ? "sales.sale_information.staircasesale" : "sales.sale_information.about_staircasing.staircasesale" + @depends_on = [{ + "staircase" => 1, + "stairowned" => 100, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::StaircaseSale.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/value_shared_ownership.rb b/app/models/form/sales/pages/value_shared_ownership.rb index 200563053..bf628b3f8 100644 --- a/app/models/form/sales/pages/value_shared_ownership.rb +++ b/app/models/form/sales/pages/value_shared_ownership.rb @@ -1,7 +1,7 @@ class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page def initialize(id, hsh, subsection) super - @id = "value_shared_ownership" + @copy_key = "sales.sale_information.value" end def questions diff --git a/app/models/form/sales/questions/equity.rb b/app/models/form/sales/questions/equity.rb index 7a2a4ce5b..e39e77ebb 100644 --- a/app/models/form/sales/questions/equity.rb +++ b/app/models/form/sales/questions/equity.rb @@ -2,6 +2,7 @@ class Form::Sales::Questions::Equity < ::Form::Question def initialize(id, hsh, page) super @id = "equity" + @copy_key = "sales.sale_information.equity.#{page.id}" @type = "numeric" @min = 0 @max = 100 diff --git a/app/models/form/sales/questions/monthly_rent_after_staircasing.rb b/app/models/form/sales/questions/monthly_rent_after_staircasing.rb new file mode 100644 index 000000000..1116abb7b --- /dev/null +++ b/app/models/form/sales/questions/monthly_rent_after_staircasing.rb @@ -0,0 +1,15 @@ +class Form::Sales::Questions::MonthlyRentAfterStaircasing < ::Form::Question + def initialize(id, hsh, page) + super + @id = "mrent" + @copy_key = "sales.sale_information.mrent_staircasing.poststaircasing" + @type = "numeric" + @min = 0 + @step = 0.01 + @width = 5 + @prefix = "£" + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 99 }.freeze +end diff --git a/app/models/form/sales/questions/monthly_rent_before_staircasing.rb b/app/models/form/sales/questions/monthly_rent_before_staircasing.rb new file mode 100644 index 000000000..a7966447a --- /dev/null +++ b/app/models/form/sales/questions/monthly_rent_before_staircasing.rb @@ -0,0 +1,15 @@ +class Form::Sales::Questions::MonthlyRentBeforeStaircasing < ::Form::Question + def initialize(id, hsh, page) + super + @id = "mrentprestaircasing" + @copy_key = "sales.sale_information.mrent_staircasing.prestaircasing" + @type = "numeric" + @min = 0 + @step = 0.01 + @width = 5 + @prefix = "£" + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 98 }.freeze +end diff --git a/app/models/form/sales/questions/mortgageused.rb b/app/models/form/sales/questions/mortgageused.rb index 3c3c42840..1c683384b 100644 --- a/app/models/form/sales/questions/mortgageused.rb +++ b/app/models/form/sales/questions/mortgageused.rb @@ -12,7 +12,7 @@ class Form::Sales::Questions::Mortgageused < ::Form::Question def displayed_answer_options(log, _user = nil) if log.outright_sale? && log.saledate && !form.start_year_2024_or_later? answer_options_without_dont_know - elsif log.stairowned == 100 || log.outright_sale? + elsif log.stairowned_100? || log.outright_sale? || (log.is_staircase? && form.start_year_2025_or_later?) ANSWER_OPTIONS else answer_options_without_dont_know diff --git a/app/models/form/sales/questions/staircase_count.rb b/app/models/form/sales/questions/staircase_count.rb new file mode 100644 index 000000000..07095cd6a --- /dev/null +++ b/app/models/form/sales/questions/staircase_count.rb @@ -0,0 +1,15 @@ +class Form::Sales::Questions::StaircaseCount < ::Form::Question + def initialize(id, hsh, page) + super + @id = "numstair" + @copy_key = "sales.sale_information.stairprevious.numstair" + @type = "numeric" + @width = 2 + @min = 2 + @max = 10 + @step = 1 + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 82 }.freeze +end diff --git a/app/models/form/sales/questions/staircase_first_time.rb b/app/models/form/sales/questions/staircase_first_time.rb new file mode 100644 index 000000000..fed2c34fb --- /dev/null +++ b/app/models/form/sales/questions/staircase_first_time.rb @@ -0,0 +1,16 @@ +class Form::Sales::Questions::StaircaseFirstTime < ::Form::Question + def initialize(id, hsh, page) + super + @id = "firststair" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + }.freeze + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 81 }.freeze +end diff --git a/app/models/form/sales/questions/staircase_initial_date.rb b/app/models/form/sales/questions/staircase_initial_date.rb new file mode 100644 index 000000000..b810d6689 --- /dev/null +++ b/app/models/form/sales/questions/staircase_initial_date.rb @@ -0,0 +1,11 @@ +class Form::Sales::Questions::StaircaseInitialDate < ::Form::Question + def initialize(id, hsh, page) + super + @id = "initialpurchase" + @copy_key = "sales.sale_information.stairprevious.initialpurchase" + @type = "date" + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 83 }.freeze +end diff --git a/app/models/form/sales/questions/staircase_last_date.rb b/app/models/form/sales/questions/staircase_last_date.rb new file mode 100644 index 000000000..edb75e823 --- /dev/null +++ b/app/models/form/sales/questions/staircase_last_date.rb @@ -0,0 +1,11 @@ +class Form::Sales::Questions::StaircaseLastDate < ::Form::Question + def initialize(id, hsh, page) + super + @id = "lasttransaction" + @copy_key = "sales.sale_information.stairprevious.lasttransaction" + @type = "date" + @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] + end + + QUESTION_NUMBER_FROM_YEAR = { 2025 => 83 }.freeze +end diff --git a/app/models/form/sales/questions/staircase_sale.rb b/app/models/form/sales/questions/staircase_sale.rb index ac54084f5..de0977ecb 100644 --- a/app/models/form/sales/questions/staircase_sale.rb +++ b/app/models/form/sales/questions/staircase_sale.rb @@ -2,7 +2,7 @@ class Form::Sales::Questions::StaircaseSale < ::Form::Question def initialize(id, hsh, page) super @id = "staircasesale" - @copy_key = "sales.sale_information.about_staircasing.staircasesale" + @copy_key = form.start_year_2025_or_later? ? "sales.sale_information.staircasesale" : "sales.sale_information.about_staircasing.staircasesale" @type = "radio" @answer_options = ANSWER_OPTIONS @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] diff --git a/app/models/form/sales/questions/value.rb b/app/models/form/sales/questions/value.rb index 1d258899d..ad021e920 100644 --- a/app/models/form/sales/questions/value.rb +++ b/app/models/form/sales/questions/value.rb @@ -2,6 +2,7 @@ class Form::Sales::Questions::Value < ::Form::Question def initialize(id, hsh, page) super @id = "value" + @copy_key = "sales.sale_information.value.#{page.id}" @type = "numeric" @min = 0 @step = 1 diff --git a/app/models/form/sales/sections/sale_information.rb b/app/models/form/sales/sections/sale_information.rb index 22dbbef5a..fc2180529 100644 --- a/app/models/form/sales/sections/sale_information.rb +++ b/app/models/form/sales/sections/sale_information.rb @@ -4,18 +4,20 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section @id = "sale_information" @label = "Sale information" @description = "" - @subsections = [ - shared_ownership_scheme_subsection, - Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self), - Form::Sales::Subsections::OutrightSale.new(nil, nil, self), - ] || [] + @subsections = [] + @subsections.concat(shared_ownership_scheme_subsection) + @subsections << Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self) + @subsections << Form::Sales::Subsections::OutrightSale.new(nil, nil, self) end def shared_ownership_scheme_subsection if form.start_year_2025_or_later? - Form::Sales::Subsections::SharedOwnershipInitialPurchase.new(nil, nil, self) + [ + Form::Sales::Subsections::SharedOwnershipInitialPurchase.new(nil, nil, self), + Form::Sales::Subsections::SharedOwnershipStaircasingTransaction.new(nil, nil, self), + ] else - Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self) + [Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)] end end end 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 5dfb322a2..175994b0b 100644 --- a/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb +++ b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb @@ -4,6 +4,7 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect @id = "shared_ownership_initial_purchase" @label = "Shared ownership - initial purchase" @depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 }] + @copy_key = "sale_information" end def pages @@ -18,9 +19,9 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousTenure.new(nil, nil, self), - Form::Sales::Pages::ValueSharedOwnership.new(nil, nil, self), + Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership", nil, self), Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self), - Form::Sales::Pages::Equity.new(nil, nil, self), + Form::Sales::Pages::Equity.new("initial_equity", nil, self), Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self), Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1), Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self), diff --git a/app/models/form/sales/subsections/shared_ownership_scheme.rb b/app/models/form/sales/subsections/shared_ownership_scheme.rb index 20a088eae..c0718e009 100644 --- a/app/models/form/sales/subsections/shared_ownership_scheme.rb +++ b/app/models/form/sales/subsections/shared_ownership_scheme.rb @@ -27,9 +27,9 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousTenure.new(nil, nil, self), - Form::Sales::Pages::ValueSharedOwnership.new(nil, nil, self), + Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership", nil, self), Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self), - Form::Sales::Pages::Equity.new(nil, nil, self), + Form::Sales::Pages::Equity.new("equity", nil, self), Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self), Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1), Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self), diff --git a/app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb b/app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb new file mode 100644 index 000000000..000a0c800 --- /dev/null +++ b/app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb @@ -0,0 +1,35 @@ +class Form::Sales::Subsections::SharedOwnershipStaircasingTransaction < ::Form::Subsection + def initialize(id, hsh, section) + super + @id = "shared_ownership_staircasing_transaction" + @label = "Shared ownership - staircasing transaction" + @depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 1 }] + @copy_key = "sale_information" + end + + def pages + @pages ||= [ + Form::Sales::Pages::AboutStaircase.new("about_staircasing_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::AboutStaircase.new("about_staircasing_not_joint_purchase", nil, self, joint_purchase: false), + Form::Sales::Pages::StaircaseSale.new(nil, nil, self), + Form::Sales::Pages::StaircaseBoughtValueCheck.new(nil, nil, self), + Form::Sales::Pages::StaircaseOwnedValueCheck.new("staircase_owned_value_check_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::StaircaseOwnedValueCheck.new("staircase_owned_value_check_not_joint_purchase", nil, self, joint_purchase: false), + Form::Sales::Pages::StaircaseFirstTime.new(nil, nil, self), + Form::Sales::Pages::StaircasePrevious.new(nil, nil, self), + Form::Sales::Pages::StaircaseInitialDate.new(nil, nil, self), + Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership_staircase", nil, self), + Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self), + Form::Sales::Pages::Equity.new("staircase_equity", nil, self), + Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self), + Form::Sales::Pages::Mortgageused.new("staircase_mortgage_used_shared_ownership", nil, self, ownershipsch: 1), + Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self), + Form::Sales::Pages::MonthlyRentStaircasing.new(nil, nil, self), + Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self), + ].compact + end + + def displayed_in_tasklist?(log) + log.staircase == 1 && (log.ownershipsch.nil? || log.ownershipsch == 1) + end +end diff --git a/app/models/location.rb b/app/models/location.rb index 12c6f2fad..c333f653f 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -2,7 +2,8 @@ class Location < ApplicationRecord validates :postcode, on: :postcode, presence: { message: I18n.t("validations.location.postcode_blank") } validate :validate_postcode, on: :postcode, if: proc { |model| model.postcode.presence } validates :location_admin_district, on: :location_admin_district, presence: { message: I18n.t("validations.location_admin_district") } - validates :units, on: :units, presence: { message: I18n.t("validations.location.units") } + validates :units, on: :units, presence: { message: I18n.t("validations.location.units.must_be_number") } + validates :units, on: :units, numericality: { greater_than_or_equal_to: 1, message: I18n.t("validations.location.units.must_be_one_or_more") } validates :type_of_unit, on: :type_of_unit, presence: { message: I18n.t("validations.location.type_of_unit") } validates :mobility_type, on: :mobility_type, presence: { message: I18n.t("validations.location.mobility_standards") } validates :startdate, on: :startdate, presence: { message: I18n.t("validations.location.startdate_invalid") } @@ -144,6 +145,29 @@ class Location < ApplicationRecord scope.pluck("ARRAY_AGG(id)") } + scope :duplicate_active_sets, lambda { + scope = active + .group(*DUPLICATE_LOCATION_ATTRIBUTES) + .where.not(scheme_id: nil) + .where.not(postcode: nil) + .where.not(mobility_type: nil) + .having( + "COUNT(*) > 1", + ) + scope.pluck("ARRAY_AGG(id)") + } + + scope :duplicate_active_sets_within_given_schemes, lambda { + scope = active + .group(*DUPLICATE_LOCATION_ATTRIBUTES - %w[scheme_id]) + .where.not(postcode: nil) + .where.not(mobility_type: nil) + .having( + "COUNT(*) > 1", + ) + scope.pluck("ARRAY_AGG(id)") + } + DUPLICATE_LOCATION_ATTRIBUTES = %w[scheme_id postcode mobility_type].freeze LOCAL_AUTHORITIES = LocalAuthority.all.map { |la| [la.name, la.code] }.to_h diff --git a/app/models/log.rb b/app/models/log.rb index dd4301550..bcbea9c92 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -126,9 +126,11 @@ class Log < ApplicationRecord end def address_options - return @address_options if @address_options + return @address_options if @address_options && @last_searched_address_string == address_string if [address_line1_input, postcode_full_input].all?(&:present?) + @last_searched_address_string = address_string + service = AddressClient.new(address_string) service.call if service.result.blank? || service.error.present? diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 01741fbc5..361aab6f6 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -557,4 +557,8 @@ class SalesLog < Log def is_resale? resale == 1 end + + def is_firststair? + firststair == 1 + end end diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 33f236374..1cd56ac7d 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -119,6 +119,22 @@ class Scheme < ApplicationRecord scope.pluck("ARRAY_AGG(id)") } + scope :duplicate_active_sets, lambda { + scope = active + .group(*DUPLICATE_SCHEME_ATTRIBUTES) + .where.not(scheme_type: nil) + .where.not(registered_under_care_act: nil) + .where.not(primary_client_group: nil) + .where.not(has_other_client_group: nil) + .where.not(secondary_client_group: nil).or(where(has_other_client_group: 0)) + .where.not(support_type: nil) + .where.not(intended_stay: nil) + .having( + "COUNT(*) > 1", + ) + scope.pluck("ARRAY_AGG(id)") + } + validate :validate_confirmed validate :validate_owning_organisation diff --git a/app/models/validations/sales/sale_information_validations.rb b/app/models/validations/sales/sale_information_validations.rb index fa095a5e2..3825271c5 100644 --- a/app/models/validations/sales/sale_information_validations.rb +++ b/app/models/validations/sales/sale_information_validations.rb @@ -35,6 +35,14 @@ module Validations::Sales::SaleInformationValidations end end + def validate_staircasing_initial_purchase_date(record) + return unless record.initialpurchase + + if record.initialpurchase < Time.zone.local(1980, 1, 1) + record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980") + end + end + def validate_previous_property_unit_type(record) return unless record.fromprop && record.frombeds @@ -351,6 +359,15 @@ module Validations::Sales::SaleInformationValidations end end + def validate_number_of_staircase_transactions(record) + return unless record.numstair + + if record.firststair == 2 && record.numstair < 2 + record.errors.add :numstair, I18n.t("validations.sales.sale_information.numstair.must_be_greater_than_one") + record.errors.add :firststair, I18n.t("validations.sales.sale_information.firststair.cannot_be_no") + end + end + def over_tolerance?(expected, actual, tolerance, strict: false) if strict (expected - actual).abs > tolerance diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 440eeb624..04b436aa5 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -12,6 +12,8 @@ <%= content_for(:title) %> + <%= render "devise/shared/links" %> + <%= f.govuk_email_field :email, label: { text: "Email address" }, autocomplete: "email", @@ -25,5 +27,3 @@ <% end %> - -<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index e463f5deb..f48e1b3af 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -7,7 +7,7 @@ <% end %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> -

You can <%= govuk_link_to "reset your password", new_password_path(resource_name) %> if you’ve forgotten it.

+

<%= govuk_link_to "Forgot password", new_password_path(resource_name) %>

<% end %> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> diff --git a/app/views/form/_checkbox_question.html.erb b/app/views/form/_checkbox_question.html.erb index 2e8585e15..b4feb12bd 100644 --- a/app/views/form/_checkbox_question.html.erb +++ b/app/views/form/_checkbox_question.html.erb @@ -6,16 +6,17 @@ hint: { text: question.hint_text&.html_safe } do %> <% after_divider = false %> - <% question.displayed_answer_options(@log).map do |key, option| %> + <% question.displayed_answer_options(@log).each_with_index do |(key, option), index| %> <% if key.starts_with?("divider") %> <% after_divider = true %> <%= f.govuk_check_box_divider %> <% else %> - <%= f.govuk_check_box question.id, key, + <%= f.govuk_check_box question.id.to_sym, key, label: { text: option["value"] }, hint: { text: option["hint"] }, checked: @log[key] == 1, exclusive: after_divider, + link_errors: index.zero? ? true : nil, **stimulus_html_attributes(question) %> <% end %> <% end %> diff --git a/app/views/form/_radio_question.html.erb b/app/views/form/_radio_question.html.erb index 41e98a1cc..bf6abb0d0 100644 --- a/app/views/form/_radio_question.html.erb +++ b/app/views/form/_radio_question.html.erb @@ -18,22 +18,24 @@ legend: legend(question, page_header, conditional), hint: { text: question.hint_text&.html_safe } do %> - <% question.displayed_answer_options(@log, current_user).map do |key, options| %> + <% question.displayed_answer_options(@log, current_user).each_with_index do |(key, options), index| %> <% if key.starts_with?("divider") %> <%= f.govuk_radio_divider %> <% else %> <% conditional_question = find_conditional_question(@page, question, key) %> <% if conditional_question.nil? %> - <%= f.govuk_radio_button question.id, + <%= f.govuk_radio_button question.id.to_sym, key, label: { text: options["value"] }, hint: { text: options["hint"] }, + link_errors: index.zero? ? true : nil, **stimulus_html_attributes(question) %> <% else %> - <%= f.govuk_radio_button question.id, + <%= f.govuk_radio_button question.id.to_sym, key, label: { text: options["value"] }, hint: { text: options["hint"] }, + link_errors: index.zero? ? true : nil, **stimulus_html_attributes(question) do %> <%= render partial: "#{conditional_question.type}_question", locals: { question: conditional_question, diff --git a/app/views/logs/_tasklist.html.erb b/app/views/logs/_tasklist.html.erb index e2f977a70..7a5e26a0e 100644 --- a/app/views/logs/_tasklist.html.erb +++ b/app/views/logs/_tasklist.html.erb @@ -8,19 +8,21 @@ <% if section.description %>

<%= section.description.html_safe %>

<% end %> - + <%= govuk_task_list(id_prefix: "logs", classes: "app-task-list__items") do |task_list| + section.subsections.each do |subsection| + next unless subsection.displayed_in_tasklist?(@log) && (subsection.applicable_questions(@log).count.positive? || !subsection.enabled?(@log)) + + subsection_status = subsection.status(@log) + task_list.with_item(classes: "#{tasklist_link_class(subsection_status)} app-task-list__item") do |item| + item.with_title(text: subsection.label, href: subsection_href(subsection, @log, current_user), classes: "app-task-list__name-and-hint--my-modifier") + if status_tag(subsection_status, "app-task-list__tag").present? + item.with_status(text: status_tag(subsection_status), classes: "app-task-list__tag") + else + item.with_status(text: status_text(subsection_status), classes: "app-task-list__tag") + end + end + end + end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 80711129e..851a9ea2c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -361,7 +361,9 @@ en: location: postcode_blank: "Enter a postcode." - units: "The units at this location must be a number." + units: + must_be_number: "The units at this location must be a number." + must_be_one_or_more: "Number of units must be at least 1." type_of_unit: "Select the most common type of unit at this location." mobility_standards: "Select the mobility standard for the majority of the units at this location." startdate_invalid: "Enter a valid day, month and year when the first property became available at this location." diff --git a/config/locales/forms/2025/sales/sale_information.en.yml b/config/locales/forms/2025/sales/sale_information.en.yml index 9a273d1c3..0535caca1 100644 --- a/config/locales/forms/2025/sales/sale_information.en.yml +++ b/config/locales/forms/2025/sales/sale_information.en.yml @@ -26,24 +26,47 @@ en: question_text: "Did the buyer live in the property before purchasing it?" about_staircasing: - page_header: "About the staircasing transaction" - stairbought: - check_answer_label: "Percentage bought in this staircasing transaction" + page_header: "About the staircasing transaction" + stairbought: + check_answer_label: "Percentage bought in this staircasing transaction" + hint_text: "" + question_text: "What percentage of the property has been bought in this staircasing transaction?" + stairowned: + joint_purchase: + check_answer_label: "Percentage the buyers now own in total" hint_text: "" - question_text: "What percentage of the property has been bought in this staircasing transaction?" - stairowned: - joint_purchase: - check_answer_label: "Percentage the buyers now own in total" - hint_text: "" - question_text: "What percentage of the property do the buyers now own in total?" - not_joint_purchase: - check_answer_label: "Percentage the buyer now owns in total" - hint_text: "" - question_text: "What percentage of the property does the buyer now own in total?" - staircasesale: - check_answer_label: "Part of a back-to-back staircasing transaction" + question_text: "What percentage of the property do the buyers now own in total?" + not_joint_purchase: + check_answer_label: "Percentage the buyer now owns in total" 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: "What percentage of the property does the buyer now own in total?" + + staircasesale: + page_header: "" + check_answer_label: "Part of a back-to-back staircasing transaction?" + hint_text: "" + question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?" + + firststair: + page_header: "" + check_answer_label: "First time staircasing?" + hint_text: "" + question_text: "Is this the first time the shared owner has engaged in staircasing in the home?" + + stairprevious: + page_header: "About previous staircasing transactions" + numstair: + check_answer_label: "Number of staircasing transactions" + hint_text: "" + question_text: "Including this time, how many times has the shared owner engaged in staircasing in the home?" + initialpurchase: + check_answer_label: "Initial staircasing transaction" + hint_text: "" + question_text: "What was the date of the initial purchase of a share in the property?" + lasttransaction: + check_answer_label: "Last staircasing transaction" + hint_text: "" + question_text: "What was the date of the last staircasing transaction?" resale: page_header: "" @@ -101,19 +124,29 @@ en: value: page_header: "About the price of the property" - check_answer_label: "Full purchase price" - hint_text: "Enter the full purchase price of the property before any discounts are applied. For shared ownership, enter the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)" - question_text: "What was the full purchase price?" + value_shared_ownership: + check_answer_label: "Full purchase price" + hint_text: "Enter the full purchase price of the property before any discounts are applied. For shared ownership, enter the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)." + question_text: "What was the full purchase price?" + value_shared_ownership_staircase: + check_answer_label: "Full purchase price" + hint_text: "Enter the full purchase price paid for the equity bought in this staircasing transaction (this is equal to the value of the share bought by the purchaser)." + question_text: "What was the full purchase price for this staircasing transaction?" equity: page_header: "About the price of the property" - check_answer_label: "Initial percentage equity share" - hint_text: "Enter the amount of initial equity share held by the purchaser (for example, 25% or 50%)" - question_text: "What was the initial percentage share purchased?" + initial_equity: + check_answer_label: "Initial percentage equity share" + hint_text: "Enter the amount of initial equity share held by the purchaser (for example, 25% or 50%)" + question_text: "What was the initial percentage share purchased?" + staircase_equity: + check_answer_label: "Initial percentage equity share" + hint_text: "Enter the amount of initial equity share held by the purchaser (for example, 25% or 50%)" + question_text: "What was the percentage shared purchased in the initial transaction?" mortgageused: page_header: "Mortgage Amount" - check_answer_label: "Mortgage used" + check_answer_label: "Mortgage used?" hint_text: "" question_text: "Was a mortgage used for the purchase of this property?" @@ -165,6 +198,17 @@ en: hint_text: "Amount paid before any charges" question_text: "What is the basic monthly rent?" + mrent_staircasing: + page_header: "Monthly rent" + prestaircasing: + check_answer_label: "Monthly rent prior to staircasing" + hint_text: "Amount paid before any charges" + question_text: "What was the basic monthly rent prior to staircasing?" + poststaircasing: + check_answer_label: "Monthly rent after staircasing" + hint_text: "Amount paid before any charges" + question_text: "What is the basic monthly rent after staircasing?" + leaseholdcharges: page_header: "" has_mscharge: @@ -199,7 +243,7 @@ en: check_answer_label: "Amount of any loan, grant or subsidy" hint_text: "For all schemes except Right to Buy (RTB), Preserved Right to Buy (PRTB), Voluntary Right to Buy (VRTB) and Rent to Buy" question_text: "What was the amount of any loan, grant, discount or subsidy given?" - + management_fee: page_header: "" has_management_fee: diff --git a/config/locales/validations/sales/sale_information.en.yml b/config/locales/validations/sales/sale_information.en.yml index 8fb7d02d4..ea17953fb 100644 --- a/config/locales/validations/sales/sale_information.en.yml +++ b/config/locales/validations/sales/sale_information.en.yml @@ -19,6 +19,8 @@ en: exdate: must_be_before_saledate: "Contract exchange date must be before sale completion date." must_be_less_than_1_year_from_saledate: "Contract exchange date must be less than 1 year before sale completion date." + initialpurchase: + must_be_after_1980: "The initial purchase date must be after January 1, 1980." fromprop: previous_property_type_bedsit: "A bedsit cannot have more than 1 bedroom." frombeds: @@ -125,3 +127,7 @@ en: postcode_full: value_over_discounted_london_max: "The percentage discount multiplied by the purchase price is %{discount_value}. This figure should not be more than £136,400 for properties in London." value_over_discounted_max: "The percentage discount multiplied by the purchase price is %{discount_value}. This figure should not be more than £102,400 for properties outside of London." + numstair: + must_be_greater_than_one: "The number of staircasing transactions must be greater than 1 when this is not the first staircasing transaction." + firststair: + cannot_be_no: "The answer to 'Is this the first staircasing transaction?' cannot be 'no' if the number of staircasing transactions is 1." diff --git a/db/migrate/20241114173226_add_fields_to_sales_log.rb b/db/migrate/20241114173226_add_fields_to_sales_log.rb new file mode 100644 index 000000000..2f7dbbd2b --- /dev/null +++ b/db/migrate/20241114173226_add_fields_to_sales_log.rb @@ -0,0 +1,11 @@ +class AddFieldsToSalesLog < ActiveRecord::Migration[7.0] + def change + change_table :sales_logs, bulk: true do |t| + t.column :firststair, :integer + t.column :numstair, :integer + t.column :mrentprestaircasing, :decimal, precision: 10, scale: 2 + t.column :lasttransaction, :datetime + t.column :initialpurchase, :datetime + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c53872020..0cdc15e9f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -761,6 +761,11 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do t.bigint "created_by_id" t.integer "has_management_fee" t.decimal "management_fee", precision: 10, scale: 2 + t.integer "firststair" + t.integer "numstair" + t.decimal "mrentprestaircasing", precision: 10, scale: 2 + t.datetime "lasttransaction" + t.datetime "initialpurchase" 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" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" diff --git a/lib/tasks/count_duplicates.rake b/lib/tasks/count_duplicates.rake index e65688b4d..76cd1d991 100644 --- a/lib/tasks/count_duplicates.rake +++ b/lib/tasks/count_duplicates.rake @@ -60,4 +60,66 @@ namespace :count_duplicates do url = storage_service.get_presigned_url(filename, 72.hours.to_i) Rails.logger.info("Download URL: #{url}") end + + desc "Count the number of duplicate active schemes per organisation" + task active_scheme_duplicates_per_org: :environment do + duplicates_csv = CSV.generate(headers: true) do |csv| + csv << ["Organisation id", "Number of duplicate sets", "Total duplicate schemes"] + + Organisation.visible.each do |organisation| + if organisation.owned_schemes.duplicate_active_sets.count.positive? + csv << [organisation.id, organisation.owned_schemes.duplicate_active_sets.count, organisation.owned_schemes.duplicate_active_sets.sum(&:size)] + end + end + end + + filename = "active-scheme-duplicates-#{Time.zone.now}.csv" + storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + storage_service.write_file(filename, "#{duplicates_csv}") + + url = storage_service.get_presigned_url(filename, 72.hours.to_i) + Rails.logger.info("Download URL: #{url}") + end + + desc "Count the number of duplicate active locations per organisation" + task active_location_duplicates_per_org: :environment do + duplicates_csv = CSV.generate(headers: true) do |csv| + csv << ["Organisation id", "Duplicate sets within individual schemes", "Duplicate locations within individual schemes", "All duplicate sets", "All duplicates"] + + Organisation.visible.each do |organisation| + duplicate_sets_within_individual_schemes = [] + + organisation.owned_schemes.each do |scheme| + duplicate_sets_within_individual_schemes += scheme.locations.duplicate_active_sets + end + duplicate_locations_within_individual_schemes = duplicate_sets_within_individual_schemes.flatten + + duplicate_sets_within_duplicate_schemes = [] + if organisation.owned_schemes.duplicate_active_sets.count.positive? + organisation.owned_schemes.duplicate_active_sets.each do |duplicate_set| + duplicate_sets_within_duplicate_schemes += Location.where(scheme_id: duplicate_set).duplicate_active_sets_within_given_schemes + end + duplicate_locations_within_duplicate_schemes_ids = duplicate_sets_within_duplicate_schemes.flatten + + duplicate_sets_within_individual_schemes_without_intersecting_sets = duplicate_sets_within_individual_schemes.reject { |set| set.any? { |id| duplicate_sets_within_duplicate_schemes.any? { |duplicate_set| duplicate_set.include?(id) } } } + all_duplicate_sets_count = (duplicate_sets_within_individual_schemes_without_intersecting_sets + duplicate_sets_within_duplicate_schemes).count + all_duplicate_locations_count = (duplicate_locations_within_duplicate_schemes_ids + duplicate_locations_within_individual_schemes).uniq.count + else + all_duplicate_sets_count = duplicate_sets_within_individual_schemes.count + all_duplicate_locations_count = duplicate_locations_within_individual_schemes.count + end + + if all_duplicate_locations_count.positive? + csv << [organisation.id, duplicate_sets_within_individual_schemes.count, duplicate_locations_within_individual_schemes.count, all_duplicate_sets_count, all_duplicate_locations_count] + end + end + end + + filename = "active-location-duplicates-#{Time.zone.now}.csv" + storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + storage_service.write_file(filename, "#{duplicates_csv}") + + url = storage_service.get_presigned_url(filename, 72.hours.to_i) + Rails.logger.info("Download URL: #{url}") + end end diff --git a/spec/factories/scheme.rb b/spec/factories/scheme.rb index 34f98a8a6..e7ecc8b60 100644 --- a/spec/factories/scheme.rb +++ b/spec/factories/scheme.rb @@ -44,5 +44,10 @@ FactoryBot.define do trait :created_now do created_at { Time.zone.now } end + trait :with_location do + after(:create) do |scheme| + create(:location, scheme:) + end + end end end diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index efb7e7665..b10dfc6e5 100644 --- a/spec/features/lettings_log_spec.rb +++ b/spec/features/lettings_log_spec.rb @@ -729,5 +729,353 @@ RSpec.describe "Lettings Log Features" do expect(duplicate_log.duplicate_set_id).to be_nil end end + + context "when filling out address fields" do + let(:lettings_log) { create(:lettings_log, :setup_completed, assigned_to: user) } + + before do + body = { + results: [ + { + DPA: { + "POSTCODE": "AA1 1AA", + "POST_TOWN": "Bristol", + "ORGANISATION_NAME": "Some place", + }, + }, + ], + }.to_json + + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111") + .to_return(status: 200, body:, headers: {}) + + body = { results: [{ DPA: { UPRN: "111" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4") + .to_return(status: 200, body:, headers: {}) + + WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA") + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {}) + + WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA") + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {}) + + body = { results: [] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4") + .to_return(status: 200, body:, headers: {}) + + visit("/lettings-logs/#{lettings_log.id}/uprn") + end + + context "and uprn is known and answered" do + before do + choose "Yes" + fill_in("lettings_log[uprn]", with: "111") + click_button("Save and continue") + end + + context "and uprn is confirmed" do + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(1) # yes + expect(lettings_log.uprn).to eq("111") + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Some Place") + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq("Bristol") + expect(lettings_log.address_line1_input).to eq(nil) + expect(lettings_log.postcode_full_input).to eq(nil) + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + + choose "Yes" + click_button("Save and continue") + + lettings_log.reload + expect(lettings_log.uprn_known).to eq(1) # yes + expect(lettings_log.uprn).to eq("111") + expect(lettings_log.uprn_confirmed).to eq(1) # yes + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Some Place") + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq("Bristol") + expect(lettings_log.address_line1_input).to eq(nil) + expect(lettings_log.postcode_full_input).to eq(nil) + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + end + + context "and changes to uprn not known" do + it "sets correct address fields" do + visit("/lettings-logs/#{lettings_log.id}/uprn") + + choose "No" + click_button("Save and continue") + + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(nil) + expect(lettings_log.postcode_full).to eq(nil) + expect(lettings_log.address_line1).to eq(nil) + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq(nil) + expect(lettings_log.postcode_full_input).to eq(nil) + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq(nil) + end + end + end + + context "and uprn is not confirmed" do + before do + choose "No, I want to search for the address instead" + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(nil) + expect(lettings_log.postcode_full).to eq(nil) + expect(lettings_log.address_line1).to eq(nil) + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq(nil) + expect(lettings_log.postcode_full_input).to eq(nil) + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq(nil) + end + end + end + + context "and uprn is not known" do + before do + choose "No" + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(nil) + expect(lettings_log.postcode_full).to eq(nil) + expect(lettings_log.address_line1).to eq(nil) + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq(nil) + expect(lettings_log.postcode_full_input).to eq(nil) + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq(nil) + end + + context "and the address is not found" do + it "sets correct address fields" do + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB") + click_button("Search") + + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(nil) + expect(lettings_log.postcode_full).to eq(nil) + expect(lettings_log.address_line1).to eq(nil) + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AB") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq(nil) + + click_button("Confirm and continue") + + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(nil) + expect(lettings_log.postcode_full).to eq(nil) + expect(lettings_log.address_line1).to eq(nil) + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AB") + expect(lettings_log.address_search_value_check).to eq(0) + expect(lettings_log.la).to eq(nil) + end + end + + context "and address is found, re-searched and not found" do + before do + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + visit("/lettings-logs/#{lettings_log.id}/address-matcher") + + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB") + click_button("Search") + end + + it "routes to the correct page" do + expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/no-address-found") + end + end + + context "and the user selects 'address_not_listed'" do + before do + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose "The address is not listed, I want to enter the address manually" + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq("uprn_not_listed") + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Address line 1") + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq(nil) + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AA") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + end + + context "and the user enters a new address manually" do + context "without changing a valid postcode" do + before do + fill_in("lettings_log[town_or_city]", with: "Town") + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq("uprn_not_listed") + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Address line 1") + expect(lettings_log.address_line2).to eq("") + expect(lettings_log.town_or_city).to eq("Town") + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AA") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + end + end + + context "with changing the postcode" do + before do + fill_in("lettings_log[town_or_city]", with: "Town") + fill_in("lettings_log[postcode_full]", with: "AA12AA") + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(0) # no + expect(lettings_log.uprn).to eq(nil) + expect(lettings_log.uprn_confirmed).to eq(nil) + expect(lettings_log.uprn_selection).to eq("uprn_not_listed") + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 2AA") + expect(lettings_log.address_line1).to eq("Address line 1") + expect(lettings_log.address_line2).to eq("") + expect(lettings_log.town_or_city).to eq("Town") + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AA") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E08000010") + end + end + end + end + + context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do + before do + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose "The address is not listed, I want to enter the address manually" + click_button("Save and continue") + + visit("/lettings-logs/#{lettings_log.id}/uprn-selection") + choose("lettings-log-uprn-selection-111-field", allow_label_click: true) + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(1) + expect(lettings_log.uprn).to eq("111") + expect(lettings_log.uprn_confirmed).to eq(1) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Some Place") + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq("Bristol") + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AA") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + end + end + + context "and possible addresses found and selected" do + before do + fill_in("lettings_log[address_line1_input]", with: "Address line 1") + fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose("lettings-log-uprn-selection-111-field", allow_label_click: true) + click_button("Save and continue") + end + + it "sets correct address fields" do + lettings_log.reload + expect(lettings_log.uprn_known).to eq(1) + expect(lettings_log.uprn).to eq("111") + expect(lettings_log.uprn_confirmed).to eq(1) + expect(lettings_log.uprn_selection).to eq(nil) + expect(lettings_log.postcode_known).to eq(1) + expect(lettings_log.postcode_full).to eq("AA1 1AA") + expect(lettings_log.address_line1).to eq("Some Place") + expect(lettings_log.address_line2).to eq(nil) + expect(lettings_log.town_or_city).to eq("Bristol") + expect(lettings_log.address_line1_input).to eq("Address line 1") + expect(lettings_log.postcode_full_input).to eq("AA1 1AA") + expect(lettings_log.address_search_value_check).to eq(nil) + expect(lettings_log.la).to eq("E09000033") + end + end + end + end end end diff --git a/spec/features/sales_log_spec.rb b/spec/features/sales_log_spec.rb index 879f2b5c8..d418bcb37 100644 --- a/spec/features/sales_log_spec.rb +++ b/spec/features/sales_log_spec.rb @@ -310,6 +310,354 @@ RSpec.describe "Sales Log Features" do expect(page).to have_current_path("/sales-logs/bulk-uploads") end end + + context "when filling out address fields" do + let(:sales_log) { create(:sales_log, :shared_ownership_setup_complete, assigned_to: user) } + + before do + body = { + results: [ + { + DPA: { + "POSTCODE": "AA1 1AA", + "POST_TOWN": "Bristol", + "ORGANISATION_NAME": "Some place", + }, + }, + ], + }.to_json + + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111") + .to_return(status: 200, body:, headers: {}) + + body = { results: [{ DPA: { UPRN: "111" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4") + .to_return(status: 200, body:, headers: {}) + + WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA") + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {}) + + WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA") + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {}) + + body = { results: [] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4") + .to_return(status: 200, body:, headers: {}) + + visit("/sales-logs/#{sales_log.id}/uprn") + end + + context "and uprn is known and answered" do + before do + choose "Yes" + fill_in("sales_log[uprn]", with: "111") + click_button("Save and continue") + end + + context "and uprn is confirmed" do + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(1) # yes + expect(sales_log.uprn).to eq("111") + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Some Place") + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq("Bristol") + expect(sales_log.address_line1_input).to eq(nil) + expect(sales_log.postcode_full_input).to eq(nil) + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + + choose "Yes" + click_button("Save and continue") + + sales_log.reload + expect(sales_log.uprn_known).to eq(1) # yes + expect(sales_log.uprn).to eq("111") + expect(sales_log.uprn_confirmed).to eq(1) # yes + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Some Place") + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq("Bristol") + expect(sales_log.address_line1_input).to eq(nil) + expect(sales_log.postcode_full_input).to eq(nil) + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + end + + context "and changes to uprn not known" do + it "sets correct address fields" do + visit("/sales-logs/#{sales_log.id}/uprn") + + choose "No" + click_button("Save and continue") + + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(nil) + expect(sales_log.postcode_full).to eq(nil) + expect(sales_log.address_line1).to eq(nil) + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq(nil) + expect(sales_log.postcode_full_input).to eq(nil) + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq(nil) + end + end + end + + context "and uprn is not confirmed" do + before do + choose "No, I want to search for the address instead" + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(nil) + expect(sales_log.postcode_full).to eq(nil) + expect(sales_log.address_line1).to eq(nil) + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq(nil) + expect(sales_log.postcode_full_input).to eq(nil) + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq(nil) + end + end + end + + context "and uprn is not known" do + before do + choose "No" + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(nil) + expect(sales_log.postcode_full).to eq(nil) + expect(sales_log.address_line1).to eq(nil) + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq(nil) + expect(sales_log.postcode_full_input).to eq(nil) + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq(nil) + end + + context "and the address is not found" do + it "sets correct address fields" do + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AB") + click_button("Search") + + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(nil) + expect(sales_log.postcode_full).to eq(nil) + expect(sales_log.address_line1).to eq(nil) + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AB") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq(nil) + + click_button("Confirm and continue") + + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(nil) + expect(sales_log.postcode_full).to eq(nil) + expect(sales_log.address_line1).to eq(nil) + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AB") + expect(sales_log.address_search_value_check).to eq(0) + expect(sales_log.la).to eq(nil) + end + end + + context "and address is found, re-searched and not found" do + before do + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + visit("/sales-logs/#{sales_log.id}/address-matcher") + + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AB") + click_button("Search") + end + + it "routes to the correct page" do + expect(page).to have_current_path("/sales-logs/#{sales_log.id}/no-address-found") + end + end + + context "and the user selects 'address_not_listed'" do + before do + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose "The address is not listed, I want to enter the address manually" + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq("uprn_not_listed") + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Address line 1") + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq(nil) + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AA") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + end + + context "and the user enters a new address manually" do + context "without changing a valid postcode" do + before do + fill_in("sales_log[town_or_city]", with: "Town") + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq("uprn_not_listed") + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Address line 1") + expect(sales_log.address_line2).to eq("") + expect(sales_log.town_or_city).to eq("Town") + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AA") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + end + end + + context "with changing the postcode" do + before do + fill_in("sales_log[town_or_city]", with: "Town") + fill_in("sales_log[postcode_full]", with: "AA12AA") + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(0) # no + expect(sales_log.uprn).to eq(nil) + expect(sales_log.uprn_confirmed).to eq(nil) + expect(sales_log.uprn_selection).to eq("uprn_not_listed") + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 2AA") + expect(sales_log.address_line1).to eq("Address line 1") + expect(sales_log.address_line2).to eq("") + expect(sales_log.town_or_city).to eq("Town") + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AA") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E08000010") + end + end + end + end + + context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do + before do + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose "The address is not listed, I want to enter the address manually" + click_button("Save and continue") + + visit("/sales-logs/#{sales_log.id}/uprn-selection") + choose("sales-log-uprn-selection-111-field", allow_label_click: true) + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(1) + expect(sales_log.uprn).to eq("111") + expect(sales_log.uprn_confirmed).to eq(1) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Some Place") + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq("Bristol") + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AA") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + end + end + + context "and possible addresses found and selected" do + before do + fill_in("sales_log[address_line1_input]", with: "Address line 1") + fill_in("sales_log[postcode_full_input]", with: "AA1 1AA") + click_button("Search") + choose("sales-log-uprn-selection-111-field", allow_label_click: true) + click_button("Save and continue") + end + + it "sets correct address fields" do + sales_log.reload + expect(sales_log.uprn_known).to eq(1) + expect(sales_log.uprn).to eq("111") + expect(sales_log.uprn_confirmed).to eq(1) + expect(sales_log.uprn_selection).to eq(nil) + expect(sales_log.pcodenk).to eq(0) + expect(sales_log.postcode_full).to eq("AA1 1AA") + expect(sales_log.address_line1).to eq("Some Place") + expect(sales_log.address_line2).to eq(nil) + expect(sales_log.town_or_city).to eq("Bristol") + expect(sales_log.address_line1_input).to eq("Address line 1") + expect(sales_log.postcode_full_input).to eq("AA1 1AA") + expect(sales_log.address_search_value_check).to eq(nil) + expect(sales_log.la).to eq("E09000033") + end + end + end + end end context "when a log becomes a duplicate" do diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index bc562824c..2e837abe2 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -61,7 +61,7 @@ RSpec.describe "User Features" do context "when the user has forgotten their password" do it "is redirected to the reset password page when they click the reset password link" do visit("/lettings-logs") - click_link("reset your password") + click_link("Forgot password") expect(page).to have_current_path("/account/password/new") end @@ -744,7 +744,7 @@ RSpec.describe "User Features" do it "is redirected to the reset password page when they click the reset password link" do visit("/account/sign-in") - click_link("reset your password") + click_link("Forgot password") expect(page).to have_current_path("/account/password/new") end diff --git a/spec/helpers/tag_helper_spec.rb b/spec/helpers/tag_helper_spec.rb index 231323278..1a6c75724 100644 --- a/spec/helpers/tag_helper_spec.rb +++ b/spec/helpers/tag_helper_spec.rb @@ -10,8 +10,8 @@ RSpec.describe TagHelper do end it "returns tag with correct status text and colour and custom class" do - expect(status_tag("not_started", "app-tag--small")).to eq("Not started") - expect(status_tag("cannot_start_yet", "app-tag--small")).to eq("Cannot start yet") + expect(status_tag("not_started", "app-tag--small")).to eq("Not started") + expect(status_tag("cannot_start_yet", "app-tag--small")).to eq(nil) expect(status_tag("in_progress", "app-tag--small")).to eq("In progress") expect(status_tag("completed", "app-tag--small")).to eq("Completed") expect(status_tag("active", "app-tag--small")).to eq("Active") diff --git a/spec/lib/tasks/count_duplicates_spec.rb b/spec/lib/tasks/count_duplicates_spec.rb index 99da5b2fb..b4f6a8db8 100644 --- a/spec/lib/tasks/count_duplicates_spec.rb +++ b/spec/lib/tasks/count_duplicates_spec.rb @@ -108,4 +108,114 @@ RSpec.describe "count_duplicates" do end end end + + describe "count_duplicates:active_scheme_duplicates_per_org", type: :task do + subject(:task) { Rake::Task["count_duplicates:active_scheme_duplicates_per_org"] } + + let(:storage_service) { instance_double(Storage::S3Service) } + let(:test_url) { "test_url" } + + before do + Rake.application.rake_require("tasks/count_duplicates") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when the rake task is run" do + context "and there are no duplicate schemes" do + before do + create(:organisation) + end + + it "creates a csv with headers only" do + expect(storage_service).to receive(:write_file).with(/scheme-duplicates-.*\.csv/, "\uFEFFOrganisation id,Number of duplicate sets,Total duplicate schemes\n") + expect(Rails.logger).to receive(:info).with("Download URL: #{test_url}") + task.invoke + end + end + + context "and there are duplicate schemes" do + let(:organisation) { create(:organisation) } + let(:organisation2) { create(:organisation) } + + before do + create_list(:scheme, 2, :duplicate, :with_location, owning_organisation: organisation) + create_list(:scheme, 3, :duplicate, :with_location, primary_client_group: "I", owning_organisation: organisation) + create_list(:scheme, 5, :duplicate, :with_location, owning_organisation: organisation2) + deactivated_schemes = create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + deactivated_schemes.each do |scheme| + create(:scheme_deactivation_period, deactivation_date: Time.zone.yesterday, reactivation_date: nil, scheme:) + end + end + + it "creates a csv with correct duplicate numbers" do + expect(storage_service).to receive(:write_file).with(/scheme-duplicates-.*\.csv/, "\uFEFFOrganisation id,Number of duplicate sets,Total duplicate schemes\n#{organisation.id},2,5\n#{organisation2.id},1,5\n") + expect(Rails.logger).to receive(:info).with("Download URL: #{test_url}") + task.invoke + end + end + end + end + + describe "count_duplicates:active_location_duplicates_per_org", type: :task do + subject(:task) { Rake::Task["count_duplicates:active_location_duplicates_per_org"] } + + let(:storage_service) { instance_double(Storage::S3Service) } + let(:test_url) { "test_url" } + + before do + Rake.application.rake_require("tasks/count_duplicates") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when the rake task is run" do + context "and there are no duplicate locations" do + before do + create(:organisation) + end + + it "creates a csv with headers only" do + expect(storage_service).to receive(:write_file).with(/location-duplicates-.*\.csv/, "\uFEFFOrganisation id,Duplicate sets within individual schemes,Duplicate locations within individual schemes,All duplicate sets,All duplicates\n") + expect(Rails.logger).to receive(:info).with("Download URL: #{test_url}") + task.invoke + end + end + + context "and there are duplicate locations" do + let(:organisation) { create(:organisation) } + let(:scheme_a) { create(:scheme, :duplicate, owning_organisation: organisation) } + let(:scheme_b) { create(:scheme, :duplicate, owning_organisation: organisation) } + let(:scheme_c) { create(:scheme, owning_organisation: organisation) } + let(:organisation2) { create(:organisation) } + let(:scheme2) { create(:scheme, owning_organisation: organisation2) } + let(:scheme3) { create(:scheme, owning_organisation: organisation2) } + + before do + create_list(:location, 2, postcode: "A1 1AB", mobility_type: "M", scheme: scheme_a) # Location A + create_list(:location, 1, postcode: "A1 1AB", mobility_type: "A", scheme: scheme_a) # Location B + + create_list(:location, 1, postcode: "A1 1AB", mobility_type: "M", scheme: scheme_b) # Location A + create_list(:location, 1, postcode: "A1 1AB", mobility_type: "A", scheme: scheme_b) # Location B + create_list(:location, 2, postcode: "A1 1AB", mobility_type: "N", scheme: scheme_b) # Location C + + create_list(:location, 2, postcode: "A1 1AB", mobility_type: "A", scheme: scheme_c) # Location B + + create_list(:location, 5, postcode: "A1 1AB", mobility_type: "M", scheme: scheme2) + create_list(:location, 2, postcode: "A1 1AB", mobility_type: "M", scheme: scheme3) + + deactivated_locations = create_list(:location, 1, postcode: "A1 1AB", mobility_type: "M", scheme: scheme_b) + deactivated_locations.each do |location| + create(:location_deactivation_period, deactivation_date: Time.zone.yesterday, reactivation_date: nil, location:) + end + end + + it "creates a csv with correct duplicate numbers" do + expect(storage_service).to receive(:write_file).with(/location-duplicates-.*\.csv/, "\uFEFFOrganisation id,Duplicate sets within individual schemes,Duplicate locations within individual schemes,All duplicate sets,All duplicates\n#{organisation.id},3,6,4,9\n#{organisation2.id},2,7,2,7\n") + expect(Rails.logger).to receive(:info).with("Download URL: #{test_url}") + task.invoke + end + end + end + end end diff --git a/spec/models/form/sales/pages/about_staircase_spec.rb b/spec/models/form/sales/pages/about_staircase_spec.rb index 181c3f695..7d8254d4b 100644 --- a/spec/models/form/sales/pages/about_staircase_spec.rb +++ b/spec/models/form/sales/pages/about_staircase_spec.rb @@ -15,6 +15,10 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do describe "questions" do let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) } + before do + allow(subsection.form).to receive(:start_year_2025_or_later?).and_return(false) + end + context "when 2022" do let(:start_date) { Time.utc(2022, 2, 8) } diff --git a/spec/models/form/sales/pages/equity_spec.rb b/spec/models/form/sales/pages/equity_spec.rb index a44085101..83a5dfaa3 100644 --- a/spec/models/form/sales/pages/equity_spec.rb +++ b/spec/models/form/sales/pages/equity_spec.rb @@ -15,10 +15,6 @@ RSpec.describe Form::Sales::Pages::Equity, type: :model do expect(page.questions.map(&:id)).to eq(%w[equity]) end - it "has the correct id" do - expect(page.id).to eq("equity") - end - it "has the correct description" do expect(page.description).to be_nil end diff --git a/spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb b/spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb new file mode 100644 index 000000000..21f0e0ee6 --- /dev/null +++ b/spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::MonthlyRentStaircasingOwned, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[mrentprestaircasing]) + end + + it "has the correct id" do + expect(page.id).to eq("monthly_rent_staircasing_owned") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { "stairowned_100?" => true }, + ]) + end +end diff --git a/spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb b/spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb new file mode 100644 index 000000000..347e105fd --- /dev/null +++ b/spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::MonthlyRentStaircasing, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[mrentprestaircasing mrent]) + end + + it "has the correct id" do + expect(page.id).to eq("monthly_rent_staircasing") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { "stairowned_100?" => false }, + ]) + end +end diff --git a/spec/models/form/sales/pages/staircase_first_time_spec.rb b/spec/models/form/sales/pages/staircase_first_time_spec.rb new file mode 100644 index 000000000..9c7d713af --- /dev/null +++ b/spec/models/form/sales/pages/staircase_first_time_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::StaircaseFirstTime, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[firststair]) + end + + it "has the correct id" do + expect(page.id).to eq("staircase_first_time") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { "staircase" => 1 }, + ]) + end +end diff --git a/spec/models/form/sales/pages/staircase_initial_date_spec.rb b/spec/models/form/sales/pages/staircase_initial_date_spec.rb new file mode 100644 index 000000000..de5806500 --- /dev/null +++ b/spec/models/form/sales/pages/staircase_initial_date_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::StaircaseInitialDate, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[initialpurchase]) + end + + it "has the correct id" do + expect(page.id).to eq("staircase_initial_date") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { "is_firststair?" => true }, + ]) + end +end diff --git a/spec/models/form/sales/pages/staircase_previous_spec.rb b/spec/models/form/sales/pages/staircase_previous_spec.rb new file mode 100644 index 000000000..336cf0454 --- /dev/null +++ b/spec/models/form/sales/pages/staircase_previous_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::StaircasePrevious, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[numstair lasttransaction initialpurchase]) + end + + it "has the correct id" do + expect(page.id).to eq("staircase_previous") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { "is_firststair?" => false }, + ]) + end +end diff --git a/spec/models/form/sales/pages/staircase_sale_spec.rb b/spec/models/form/sales/pages/staircase_sale_spec.rb new file mode 100644 index 000000000..17a140797 --- /dev/null +++ b/spec/models/form/sales/pages/staircase_sale_spec.rb @@ -0,0 +1,38 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::StaircaseSale, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) } + + before do + allow(subsection.form).to receive(:start_year_2025_or_later?).and_return(true) + end + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[staircasesale]) + end + + it "has the correct id" do + expect(page.id).to eq("staircase_sale") + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { + "staircase" => 1, + "stairowned" => 100, + }, + ]) + end +end diff --git a/spec/models/form/sales/pages/value_shared_ownership_spec.rb b/spec/models/form/sales/pages/value_shared_ownership_spec.rb index f8232894b..eb1b1099f 100644 --- a/spec/models/form/sales/pages/value_shared_ownership_spec.rb +++ b/spec/models/form/sales/pages/value_shared_ownership_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe Form::Sales::Pages::ValueSharedOwnership, type: :model do subject(:page) { described_class.new(page_id, page_definition, subsection) } - let(:page_id) { nil } + let(:page_id) { "value_shared_ownership" } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) } diff --git a/spec/models/form/sales/questions/equity_spec.rb b/spec/models/form/sales/questions/equity_spec.rb index f58c042f5..5083af9e8 100644 --- a/spec/models/form/sales/questions/equity_spec.rb +++ b/spec/models/form/sales/questions/equity_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Questions::Equity, type: :model do let(:question_id) { nil } let(:question_definition) { nil } - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } + let(:page) { instance_double(Form::Page, id: "initial_equity", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } it "has correct page" do expect(question.page).to eq(page) diff --git a/spec/models/form/sales/questions/monthly_rent_after_staircasing_spec.rb b/spec/models/form/sales/questions/monthly_rent_after_staircasing_spec.rb new file mode 100644 index 000000000..4ceb3df00 --- /dev/null +++ b/spec/models/form/sales/questions/monthly_rent_after_staircasing_spec.rb @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::MonthlyRentAfterStaircasing, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("mrent") + end + + it "has the correct type" do + expect(question.type).to eq("numeric") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has correct width" do + expect(question.width).to eq(5) + end + + it "has correct prefix" do + expect(question.prefix).to eq("£") + end + + it "has correct min" do + expect(question.min).to eq(0) + end +end diff --git a/spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb b/spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb new file mode 100644 index 000000000..8d7d864a8 --- /dev/null +++ b/spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::MonthlyRentBeforeStaircasing, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("mrentprestaircasing") + end + + it "has the correct type" do + expect(question.type).to eq("numeric") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has correct width" do + expect(question.width).to eq(5) + end + + it "has correct prefix" do + expect(question.prefix).to eq("£") + end + + it "has correct min" do + expect(question.min).to eq(0) + end +end diff --git a/spec/models/form/sales/questions/mortgageused_spec.rb b/spec/models/form/sales/questions/mortgageused_spec.rb index e85238a4d..971eb2909 100644 --- a/spec/models/form/sales/questions/mortgageused_spec.rb +++ b/spec/models/form/sales/questions/mortgageused_spec.rb @@ -3,28 +3,39 @@ require "rails_helper" RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch:) } - let(:ownershipsch) { 1 } let(:question_id) { nil } let(:question_definition) { nil } - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) } - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:)) } let(:stairowned) { nil } let(:staircase) { nil } let(:saledate) { Time.zone.today } let(:log) { build(:sales_log, :in_progress, ownershipsch:, stairowned:, staircase:) } - it "has the correct answer_options" do - expect(question.answer_options).to eq({ - "1" => { "value" => "Yes" }, - "2" => { "value" => "No" }, - "3" => { "value" => "Don’t know" }, - }) - end + context "when the form start year is 2024" do + let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:)) } + let(:saledate) { Time.zone.local(2024, 5, 1) } + let(:ownershipsch) { 1 } + + before do + allow(form).to receive(:start_year_2024_or_later?).and_return true + allow(form).to receive(:start_year_2025_or_later?).and_return false + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + "3" => { "value" => "Don’t know" }, + }) + end - describe "the displayed answer options" do context "when it is a discounted ownership sale" do let(:ownershipsch) { 2 } + it "shows the correct question number" do + expect(question.question_number).to eq 104 + end + it "does not show the don't know option" do expect_the_question_not_to_show_dont_know end @@ -34,20 +45,14 @@ RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do let(:ownershipsch) { 3 } context "and the saledate is before 24/25" do - before do - allow(form).to receive(:start_year_2024_or_later?).and_return false - end + let(:saledate) { Time.zone.local(2023, 5, 1) }\ - it "does not show the don't know option" do - expect_the_question_not_to_show_dont_know + it "does show the don't know option" do + expect_the_question_to_show_dont_know end end - context "and the saledate is 24/25 or after" do - before do - allow(form).to receive(:start_year_2024_or_later?).and_return true - end - + context "and the saledate is 24/25" do it "shows the don't know option" do expect_the_question_to_show_dont_know end @@ -87,6 +92,57 @@ RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do end end + context "when the form start year is 2025" do + let(:form) { instance_double(Form, start_date: Time.zone.local(2025, 4, 1)) } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:)) } + let(:saledate) { Time.zone.local(2025, 5, 1) } + + before do + allow(form).to receive(:start_year_2024_or_later?).and_return true + allow(form).to receive(:start_year_2025_or_later?).and_return true + end + + context "when it is a discounted ownership sale" do + let(:ownershipsch) { 2 } + + it "shows the correct question number" do + expect(question.question_number).to eq 104 + end + + it "does not show the don't know option" do + expect_the_question_not_to_show_dont_know + end + end + + context "when it is a shared ownership scheme" do + let(:ownershipsch) { 1 } + + context "and it is a staircasing transaction" do + let(:staircase) { 1 } + + it "does show the don't know option" do + expect_the_question_to_show_dont_know + end + + context "and stairowned is 100" do + let(:stairowned) { 100 } + + it "shows the don't know option" do + expect_the_question_to_show_dont_know + end + end + end + + context "and it is not a staircasing transaction" do + let(:staircase) { 2 } + + it "does not show the don't know option" do + expect_the_question_not_to_show_dont_know + end + end + end + end + private def expect_the_question_not_to_show_dont_know diff --git a/spec/models/form/sales/questions/staircase_count_spec.rb b/spec/models/form/sales/questions/staircase_count_spec.rb new file mode 100644 index 000000000..06f39b3d0 --- /dev/null +++ b/spec/models/form/sales/questions/staircase_count_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::StaircaseCount, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1)))) } + + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(true) + end + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("numstair") + end + + it "has the correct type" do + expect(question.type).to eq("numeric") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq(nil) + end +end diff --git a/spec/models/form/sales/questions/staircase_first_time_spec.rb b/spec/models/form/sales/questions/staircase_first_time_spec.rb new file mode 100644 index 000000000..59d0281a2 --- /dev/null +++ b/spec/models/form/sales/questions/staircase_first_time_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::StaircaseFirstTime, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1)))) } + + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(true) + end + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("firststair") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + }) + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq(nil) + end +end diff --git a/spec/models/form/sales/questions/staircase_initial_date_spec.rb b/spec/models/form/sales/questions/staircase_initial_date_spec.rb new file mode 100644 index 000000000..3c244e512 --- /dev/null +++ b/spec/models/form/sales/questions/staircase_initial_date_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::StaircaseInitialDate, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1)))) } + + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(true) + end + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("initialpurchase") + end + + it "has the correct type" do + expect(question.type).to eq("date") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq(nil) + end +end diff --git a/spec/models/form/sales/questions/staircase_last_date_spec.rb b/spec/models/form/sales/questions/staircase_last_date_spec.rb new file mode 100644 index 000000000..2efa5ccc7 --- /dev/null +++ b/spec/models/form/sales/questions/staircase_last_date_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::StaircaseLastDate, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2025, 4, 1)))) } + + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(true) + end + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("lasttransaction") + end + + it "has the correct type" do + expect(question.type).to eq("date") + end + + it "is not marked as derived" do + expect(question.derived?(nil)).to be false + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq(nil) + end +end diff --git a/spec/models/form/sales/questions/staircase_sale_spec.rb b/spec/models/form/sales/questions/staircase_sale_spec.rb index d330aecb3..8d3d1e5da 100644 --- a/spec/models/form/sales/questions/staircase_sale_spec.rb +++ b/spec/models/form/sales/questions/staircase_sale_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Questions::StaircaseSale, type: :model do 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)))) } + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false) + end + it "has correct page" do expect(question.page).to eq(page) end diff --git a/spec/models/form/sales/questions/value_spec.rb b/spec/models/form/sales/questions/value_spec.rb index 771607c16..10f281e63 100644 --- a/spec/models/form/sales/questions/value_spec.rb +++ b/spec/models/form/sales/questions/value_spec.rb @@ -5,7 +5,11 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do let(:question_id) { nil } let(:question_definition) { nil } - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } + let(:page) { instance_double(Form::Page, id: "value_shared_ownership", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } + + before do + allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false) + end it "has correct page" do expect(question.page).to eq(page) diff --git a/spec/models/form/sales/sections/sale_information_spec.rb b/spec/models/form/sales/sections/sale_information_spec.rb index 8167c92a3..191bca4bb 100644 --- a/spec/models/form/sales/sections/sale_information_spec.rb +++ b/spec/models/form/sales/sections/sale_information_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Sections::SaleInformation, type: :model do let(:section_definition) { nil } let(:form) { instance_double(Form, start_year_2025_or_later?: false) } + before do + allow(form).to receive(:start_year_2025_or_later?).and_return(false) + end + it "has correct form" do expect(sale_information.form).to eq(form) end @@ -22,11 +26,16 @@ RSpec.describe Form::Sales::Sections::SaleInformation, type: :model do end context "when form is 2025 or later" do - let(:form) { instance_double(Form, start_year_2025_or_later?: true) } + let(:form) { instance_double(Form) } + + before do + allow(form).to receive(:start_year_2025_or_later?).and_return(true) + end it "has correct subsections" do expect(sale_information.subsections.map(&:id)).to eq(%w[ shared_ownership_initial_purchase + shared_ownership_staircasing_transaction discounted_ownership_scheme outright_sale ]) 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 3b2d72b01..3e473a162 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 @@ -30,7 +30,7 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipInitialPurchase, type: : shared_ownership_previous_tenure value_shared_ownership about_price_shared_ownership_value_check - equity + initial_equity shared_ownership_equity_value_check mortgage_used_shared_ownership mortgage_used_mortgage_value_check diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 18581fb6e..c220914b7 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -740,10 +740,38 @@ RSpec.describe Location, type: :model do describe "#units" do let(:location) { FactoryBot.build(:location) } - it "does add an error when the number of units is invalid" do - location.units = nil - location.valid?(:units) - expect(location.errors.count).to eq(1) + context "when the number of units is invalid" do + it "adds an error when units is nil" do + location.units = nil + location.valid?(:units) + expect(location.errors.count).to eq(2) + end + + it "adds an error when units is 0" do + location.units = 0 + location.valid?(:units) + expect(location.errors.count).to eq(1) + end + end + + context "when the number of units is valid" do + it "does not add an error when units is 1" do + location.units = 1 + location.valid?(:units) + expect(location.errors.count).to eq(0) + end + + it "does not add an error when units is 10" do + location.units = 10 + location.valid?(:units) + expect(location.errors.count).to eq(0) + end + + it "does not add an error when units is 200" do + location.units = 200 + location.valid?(:units) + expect(location.errors.count).to eq(0) + end end end diff --git a/spec/models/validations/sales/sale_information_validations_spec.rb b/spec/models/validations/sales/sale_information_validations_spec.rb index 5cc0cdf07..fabe3c8c5 100644 --- a/spec/models/validations/sales/sale_information_validations_spec.rb +++ b/spec/models/validations/sales/sale_information_validations_spec.rb @@ -1407,4 +1407,44 @@ RSpec.describe Validations::Sales::SaleInformationValidations do end end end + + describe "#validate_number_of_staircase_transactions" do + let(:record) { build(:sales_log, numstair:, firststair:) } + + before do + sale_information_validator.validate_number_of_staircase_transactions(record) + end + + context "when it is not the first staircasing transaction" do + context "and the number of staircasing transactions is between 2 and 10" do + let(:numstair) { 6 } + let(:firststair) { 2 } + + it "does not add an error" do + expect(record.errors).to be_empty + end + end + + context "and the number of staircasing transactions is less than 2" do + let(:numstair) { 1 } + let(:firststair) { 2 } + + it "adds an error" do + expect(record.errors[:numstair]).to include(I18n.t("validations.sales.sale_information.numstair.must_be_greater_than_one")) + expect(record.errors[:firststair]).to include(I18n.t("validations.sales.sale_information.firststair.cannot_be_no")) + end + end + end + + context "when it is the first staircasing transaction" do + context "and numstair is also 1" do + let(:numstair) { 1 } + let(:firststair) { 1 } + + it "does not add an error" do + expect(record.errors).to be_empty + end + end + end + end end diff --git a/spec/models/validations/setup_validations_spec.rb b/spec/models/validations/setup_validations_spec.rb index 5b4f03365..1104cc23d 100644 --- a/spec/models/validations/setup_validations_spec.rb +++ b/spec/models/validations/setup_validations_spec.rb @@ -703,7 +703,7 @@ RSpec.describe Validations::SetupValidations do .to include("This location is incomplete. Select another location or update this one.") end - it "produces no error when location is completes" do + it "produces no error when location is complete" do location.update!(units: 1) location.reload record.location = location diff --git a/spec/requests/form_controller_spec.rb b/spec/requests/form_controller_spec.rb index 13d711c20..e727d8acc 100644 --- a/spec/requests/form_controller_spec.rb +++ b/spec/requests/form_controller_spec.rb @@ -582,8 +582,9 @@ RSpec.describe FormController, type: :request do allow(Rails.logger).to receive(:info) end - it "re-renders the same page with errors if validation fails" do + it "redirects to the same page with errors if validation fails" do post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params + follow_redirect! expect(page).to have_content("There is a problem") expect(page).to have_content("Error: What is the tenant’s age?") end @@ -591,6 +592,8 @@ RSpec.describe FormController, type: :request do it "resets errors when fixed" do post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: valid_params + follow_redirect! + expect(page).not_to have_content("There is a problem") get "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}" expect(page).not_to have_content("There is a problem") end @@ -616,6 +619,7 @@ RSpec.describe FormController, type: :request do it "validates the date correctly" do post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params + follow_redirect! expect(page).to have_content("There is a problem") end end @@ -693,6 +697,7 @@ RSpec.describe FormController, type: :request do it "validates the date correctly" do post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params + follow_redirect! expect(page).to have_content("There is a problem") end end @@ -713,6 +718,7 @@ RSpec.describe FormController, type: :request do it "validates the date correctly" do post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params: params + follow_redirect! expect(page).to have_content("There is a problem") end end @@ -748,8 +754,9 @@ RSpec.describe FormController, type: :request do Timecop.unfreeze end - it "re-renders the same page with errors if validation fails" do + it "redirects the same page with errors if validation fails" do post "/lettings-logs/#{lettings_log.id}/managing-organisation", params: params + follow_redirect! expect(page).to have_content("There is a problem") expect(page).to have_content("Error: Which organisation manages this letting?") end