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 %>
-
- <% section.subsections.each do |subsection| %>
- <% if subsection.displayed_in_tasklist?(@log) && (subsection.applicable_questions(@log).count > 0 || !subsection.enabled?(@log)) %>
- <% subsection_status = subsection.status(@log) %>
- -
-
- <%= subsection_link(subsection, @log, current_user) %>
-
- <%= status_tag(subsection_status, "app-task-list__tag") %>
-
- <% end %>
- <% 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