Browse Source

Merge branch 'main' into CLDC-3740-Replace-you-didnt-answer-with-link

# Conflicts:
#	config/locales/forms/2025/sales/sale_information.en.yml
CLDC-3740-Replace-you-didnt-answer-with-link
Manny Dinssa 4 weeks ago
parent
commit
5f212e7ebc
  1. 2
      Dockerfile
  2. 34
      app/controllers/form_controller.rb
  3. 3
      app/models/derived_variables/sales_log_variables.rb
  4. 2
      app/models/form/sales/pages/about_staircase.rb
  5. 2
      app/models/form/sales/pages/equity.rb
  6. 17
      app/models/form/sales/pages/monthly_rent_staircasing.rb
  7. 17
      app/models/form/sales/pages/monthly_rent_staircasing_owned.rb
  8. 15
      app/models/form/sales/pages/staircase_first_time.rb
  9. 16
      app/models/form/sales/pages/staircase_initial_date.rb
  10. 18
      app/models/form/sales/pages/staircase_previous.rb
  11. 17
      app/models/form/sales/pages/staircase_sale.rb
  12. 2
      app/models/form/sales/pages/value_shared_ownership.rb
  13. 1
      app/models/form/sales/questions/equity.rb
  14. 15
      app/models/form/sales/questions/monthly_rent_after_staircasing.rb
  15. 15
      app/models/form/sales/questions/monthly_rent_before_staircasing.rb
  16. 2
      app/models/form/sales/questions/mortgageused.rb
  17. 15
      app/models/form/sales/questions/staircase_count.rb
  18. 16
      app/models/form/sales/questions/staircase_first_time.rb
  19. 11
      app/models/form/sales/questions/staircase_initial_date.rb
  20. 11
      app/models/form/sales/questions/staircase_last_date.rb
  21. 2
      app/models/form/sales/questions/staircase_sale.rb
  22. 1
      app/models/form/sales/questions/value.rb
  23. 16
      app/models/form/sales/sections/sale_information.rb
  24. 5
      app/models/form/sales/subsections/shared_ownership_initial_purchase.rb
  25. 4
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  26. 35
      app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb
  27. 3
      app/models/location.rb
  28. 4
      app/models/sales_log.rb
  29. 17
      app/models/validations/sales/sale_information_validations.rb
  30. 4
      app/views/devise/sessions/new.html.erb
  31. 2
      app/views/devise/shared/_links.html.erb
  32. 5
      app/views/form/_checkbox_question.html.erb
  33. 8
      app/views/form/_radio_question.html.erb
  34. 4
      config/locales/en.yml
  35. 54
      config/locales/forms/2025/sales/sale_information.en.yml
  36. 6
      config/locales/validations/sales/sale_information.en.yml
  37. 11
      db/migrate/20241114173226_add_fields_to_sales_log.rb
  38. 5
      db/schema.rb
  39. 4
      spec/features/user_spec.rb
  40. 4
      spec/models/form/sales/pages/about_staircase_spec.rb
  41. 4
      spec/models/form/sales/pages/equity_spec.rb
  42. 31
      spec/models/form/sales/pages/monthly_rent_staircasing_owned_spec.rb
  43. 31
      spec/models/form/sales/pages/monthly_rent_staircasing_spec.rb
  44. 31
      spec/models/form/sales/pages/staircase_first_time_spec.rb
  45. 31
      spec/models/form/sales/pages/staircase_initial_date_spec.rb
  46. 31
      spec/models/form/sales/pages/staircase_previous_spec.rb
  47. 38
      spec/models/form/sales/pages/staircase_sale_spec.rb
  48. 2
      spec/models/form/sales/pages/value_shared_ownership_spec.rb
  49. 2
      spec/models/form/sales/questions/equity_spec.rb
  50. 37
      spec/models/form/sales/questions/monthly_rent_after_staircasing_spec.rb
  51. 37
      spec/models/form/sales/questions/monthly_rent_before_staircasing_spec.rb
  52. 84
      spec/models/form/sales/questions/mortgageused_spec.rb
  53. 33
      spec/models/form/sales/questions/staircase_count_spec.rb
  54. 40
      spec/models/form/sales/questions/staircase_first_time_spec.rb
  55. 33
      spec/models/form/sales/questions/staircase_initial_date_spec.rb
  56. 33
      spec/models/form/sales/questions/staircase_last_date_spec.rb
  57. 4
      spec/models/form/sales/questions/staircase_sale_spec.rb
  58. 6
      spec/models/form/sales/questions/value_spec.rb
  59. 11
      spec/models/form/sales/sections/sale_information_spec.rb
  60. 2
      spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb
  61. 30
      spec/models/location_spec.rb
  62. 40
      spec/models/validations/sales/sale_information_validations_spec.rb
  63. 2
      spec/models/validations/setup_validations_spec.rb
  64. 11
      spec/requests/form_controller_spec.rb

2
Dockerfile

@ -10,7 +10,7 @@ RUN apk add --update --no-cache tzdata && \
# build-base: compilation tools for bundle # build-base: compilation tools for bundle
# yarn: node package manager # yarn: node package manager
# postgresql-dev: postgres driver and libraries # 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 # Bundler version should be the same version as what the Gemfile.lock was bundled with
RUN gem install bundler:2.3.14 --no-document RUN gem install bundler:2.3.14 --no-document

34
app/controllers/form_controller.rb

@ -5,6 +5,7 @@ class FormController < ApplicationController
before_action :find_resource, only: %i[review] before_action :find_resource, only: %i[review]
before_action :find_resource_by_named_id, except: %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 :check_collection_period, only: %i[submit_form show_page]
before_action :set_cache_headers, only: [:show_page]
def submit_form def submit_form
if @log if @log
@ -37,8 +38,9 @@ class FormController < ApplicationController
error_attributes = @log.errors.map(&:attribute) error_attributes = @log.errors.map(&:attribute)
Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}" Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}"
@subsection = form.subsection_for_page(@page) @subsection = form.subsection_for_page(@page)
restore_error_field_values(@page&.questions) flash[:errors] = @log.errors
render "form/page" 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 end
else else
render_not_found render_not_found
@ -84,6 +86,10 @@ class FormController < ApplicationController
@questions = request.params["related_question_ids"].map { |id| @log.form.get_question(id, @log) } @questions = request.params["related_question_ids"].map { |id| @log.form.get_question(id, @log) }
render "form/check_errors" render "form/check_errors"
else else
if flash[:errors].present?
restore_previous_errors(flash[:errors])
restore_error_field_values(flash[:log_data])
end
render "form/page" render "form/page"
end end
else else
@ -96,13 +102,21 @@ class FormController < ApplicationController
private private
def restore_error_field_values(questions) def restore_error_field_values(previous_responses)
return unless questions return unless previous_responses
previous_responses_to_reset = previous_responses.reject do |key, _|
@log.form.get_question(key, @log)&.type == "date"
end
questions.each do |question| @log.assign_attributes(previous_responses_to_reset)
if question&.type == "date" && @log.attributes.key?(question.id)
@log[question.id] = @log.send("#{question.id}_was")
end 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
end end
@ -431,4 +445,10 @@ private
def updated_answer_from_check_errors_page? def updated_answer_from_check_errors_page?
params["check_errors"] params["check_errors"]
end 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 end

3
app/models/derived_variables/sales_log_variables.rb

@ -80,6 +80,9 @@ module DerivedVariables::SalesLogVariables
self.is_la_inferred = false self.is_la_inferred = false
end end
self.numstair = is_firststair? ? 1 : nil if numstair == 1 && firststair_changed?
self.mrent = 0 if stairowned_100?
set_encoded_derived_values!(DEPENDENCIES) set_encoded_derived_values!(DEPENDENCIES)
end end

2
app/models/form/sales/pages/about_staircase.rb

@ -18,7 +18,7 @@ class Form::Sales::Pages::AboutStaircase < ::Form::Page
end end
def staircase_sale_question 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) Form::Sales::Questions::StaircaseSale.new(nil, nil, self)
end end
end end

2
app/models/form/sales/pages/equity.rb

@ -1,7 +1,7 @@
class Form::Sales::Pages::Equity < ::Form::Page class Form::Sales::Pages::Equity < ::Form::Page
def initialize(id, hsh, subsection) def initialize(id, hsh, subsection)
super super
@id = "equity" @copy_key = "sales.sale_information.equity"
end end
def questions def questions

17
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

17
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

15
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

16
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

18
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

17
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

2
app/models/form/sales/pages/value_shared_ownership.rb

@ -1,7 +1,7 @@
class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page
def initialize(id, hsh, subsection) def initialize(id, hsh, subsection)
super super
@id = "value_shared_ownership" @copy_key = "sales.sale_information.value"
end end
def questions def questions

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

@ -2,6 +2,7 @@ class Form::Sales::Questions::Equity < ::Form::Question
def initialize(id, hsh, page) def initialize(id, hsh, page)
super super
@id = "equity" @id = "equity"
@copy_key = "sales.sale_information.equity.#{page.id}"
@type = "numeric" @type = "numeric"
@min = 0 @min = 0
@max = 100 @max = 100

15
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

15
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

2
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) def displayed_answer_options(log, _user = nil)
if log.outright_sale? && log.saledate && !form.start_year_2024_or_later? if log.outright_sale? && log.saledate && !form.start_year_2024_or_later?
answer_options_without_dont_know 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 ANSWER_OPTIONS
else else
answer_options_without_dont_know answer_options_without_dont_know

15
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

16
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

11
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

11
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

2
app/models/form/sales/questions/staircase_sale.rb

@ -2,7 +2,7 @@ class Form::Sales::Questions::StaircaseSale < ::Form::Question
def initialize(id, hsh, page) def initialize(id, hsh, page)
super super
@id = "staircasesale" @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" @type = "radio"
@answer_options = ANSWER_OPTIONS @answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]

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

@ -2,6 +2,7 @@ class Form::Sales::Questions::Value < ::Form::Question
def initialize(id, hsh, page) def initialize(id, hsh, page)
super super
@id = "value" @id = "value"
@copy_key = "sales.sale_information.value.#{page.id}"
@type = "numeric" @type = "numeric"
@min = 0 @min = 0
@step = 1 @step = 1

16
app/models/form/sales/sections/sale_information.rb

@ -4,18 +4,20 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section
@id = "sale_information" @id = "sale_information"
@label = "Sale information" @label = "Sale information"
@description = "" @description = ""
@subsections = [ @subsections = []
shared_ownership_scheme_subsection, @subsections.concat(shared_ownership_scheme_subsection)
Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self), @subsections << Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self)
Form::Sales::Subsections::OutrightSale.new(nil, nil, self), @subsections << Form::Sales::Subsections::OutrightSale.new(nil, nil, self)
] || []
end end
def shared_ownership_scheme_subsection def shared_ownership_scheme_subsection
if form.start_year_2025_or_later? 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 else
Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self) [Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)]
end end
end end
end end

5
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" @id = "shared_ownership_initial_purchase"
@label = "Shared ownership - initial purchase" @label = "Shared ownership - initial purchase"
@depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 }] @depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 }]
@copy_key = "sale_information"
end end
def pages def pages
@ -18,9 +19,9 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self), Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.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::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::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::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self), Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self),

4
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::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.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::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::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::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self), Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self),

35
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

3
app/models/location.rb

@ -2,7 +2,8 @@ class Location < ApplicationRecord
validates :postcode, on: :postcode, presence: { message: I18n.t("validations.location.postcode_blank") } validates :postcode, on: :postcode, presence: { message: I18n.t("validations.location.postcode_blank") }
validate :validate_postcode, on: :postcode, if: proc { |model| model.postcode.presence } 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 :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 :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 :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") } validates :startdate, on: :startdate, presence: { message: I18n.t("validations.location.startdate_invalid") }

4
app/models/sales_log.rb

@ -557,4 +557,8 @@ class SalesLog < Log
def is_resale? def is_resale?
resale == 1 resale == 1
end end
def is_firststair?
firststair == 1
end
end end

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

@ -35,6 +35,14 @@ module Validations::Sales::SaleInformationValidations
end end
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) def validate_previous_property_unit_type(record)
return unless record.fromprop && record.frombeds return unless record.fromprop && record.frombeds
@ -351,6 +359,15 @@ module Validations::Sales::SaleInformationValidations
end end
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) def over_tolerance?(expected, actual, tolerance, strict: false)
if strict if strict
(expected - actual).abs > tolerance (expected - actual).abs > tolerance

4
app/views/devise/sessions/new.html.erb

@ -12,6 +12,8 @@
<%= content_for(:title) %> <%= content_for(:title) %>
</h1> </h1>
<%= render "devise/shared/links" %>
<%= f.govuk_email_field :email, <%= f.govuk_email_field :email,
label: { text: "Email address" }, label: { text: "Email address" },
autocomplete: "email", autocomplete: "email",
@ -25,5 +27,3 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<%= render "devise/shared/links" %>

2
app/views/devise/shared/_links.html.erb

@ -7,7 +7,7 @@
<% end %> <% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<p class="govuk-body">You can <%= govuk_link_to "reset your password", new_password_path(resource_name) %> if you’ve forgotten it.</p> <p class="govuk-body"><%= govuk_link_to "Forgot password", new_password_path(resource_name) %></p>
<% end %> <% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>

5
app/views/form/_checkbox_question.html.erb

@ -6,16 +6,17 @@
hint: { text: question.hint_text&.html_safe } do %> hint: { text: question.hint_text&.html_safe } do %>
<% after_divider = false %> <% 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") %> <% if key.starts_with?("divider") %>
<% after_divider = true %> <% after_divider = true %>
<%= f.govuk_check_box_divider %> <%= f.govuk_check_box_divider %>
<% else %> <% else %>
<%= f.govuk_check_box question.id, key, <%= f.govuk_check_box question.id.to_sym, key,
label: { text: option["value"] }, label: { text: option["value"] },
hint: { text: option["hint"] }, hint: { text: option["hint"] },
checked: @log[key] == 1, checked: @log[key] == 1,
exclusive: after_divider, exclusive: after_divider,
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) %> **stimulus_html_attributes(question) %>
<% end %> <% end %>
<% end %> <% end %>

8
app/views/form/_radio_question.html.erb

@ -18,22 +18,24 @@
legend: legend(question, page_header, conditional), legend: legend(question, page_header, conditional),
hint: { text: question.hint_text&.html_safe } do %> 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") %> <% if key.starts_with?("divider") %>
<%= f.govuk_radio_divider %> <%= f.govuk_radio_divider %>
<% else %> <% else %>
<% conditional_question = find_conditional_question(@page, question, key) %> <% conditional_question = find_conditional_question(@page, question, key) %>
<% if conditional_question.nil? %> <% if conditional_question.nil? %>
<%= f.govuk_radio_button question.id, <%= f.govuk_radio_button question.id.to_sym,
key, key,
label: { text: options["value"] }, label: { text: options["value"] },
hint: { text: options["hint"] }, hint: { text: options["hint"] },
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) %> **stimulus_html_attributes(question) %>
<% else %> <% else %>
<%= f.govuk_radio_button question.id, <%= f.govuk_radio_button question.id.to_sym,
key, key,
label: { text: options["value"] }, label: { text: options["value"] },
hint: { text: options["hint"] }, hint: { text: options["hint"] },
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) do %> **stimulus_html_attributes(question) do %>
<%= render partial: "#{conditional_question.type}_question", locals: { <%= render partial: "#{conditional_question.type}_question", locals: {
question: conditional_question, question: conditional_question,

4
config/locales/en.yml

@ -361,7 +361,9 @@ en:
location: location:
postcode_blank: "Enter a postcode." 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." 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." 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." startdate_invalid: "Enter a valid day, month and year when the first property became available at this location."

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

@ -47,12 +47,39 @@ en:
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "What percentage of the property does the buyer now own in total?" question_text: "What percentage of the property does the buyer now own in total?"
staircasesale: staircasesale:
page_header: ""
check_answer_label: "Part of a back-to-back staircasing transaction" check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?" question_text: "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"
check_answer_prompt: ""
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"
check_answer_prompt: ""
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"
check_answer_prompt: ""
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"
check_answer_prompt: ""
hint_text: ""
question_text: "What was the date of the last staircasing transaction?"
resale: resale:
page_header: "" page_header: ""
check_answer_label: "Is this a resale?" check_answer_label: "Is this a resale?"
@ -118,17 +145,29 @@ en:
value: value:
page_header: "About the price of the property" page_header: "About the price of the property"
value_shared_ownership:
check_answer_label: "Full purchase price" check_answer_label: "Full purchase price"
check_answer_prompt: "" check_answer_prompt: ""
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)" 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?" question_text: "What was the full purchase price?"
value_shared_ownership_staircase:
check_answer_label: "Full purchase price"
check_answer_prompt: ""
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: equity:
page_header: "About the price of the property" page_header: "About the price of the property"
initial_equity:
check_answer_label: "Initial percentage equity share" check_answer_label: "Initial percentage equity share"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "Enter the amount of initial equity share held by the purchaser (for example, 25% or 50%)" 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?" question_text: "What was the initial percentage share purchased?"
staircase_equity:
check_answer_label: "Initial percentage equity share"
check_answer_prompt: ""
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: mortgageused:
page_header: "Mortgage Amount" page_header: "Mortgage Amount"
@ -193,6 +232,19 @@ en:
hint_text: "Amount paid before any charges" hint_text: "Amount paid before any charges"
question_text: "What is the basic monthly rent?" question_text: "What is the basic monthly rent?"
mrent_staircasing:
page_header: "Monthly rent"
prestaircasing:
check_answer_label: "Monthly rent prior to staircasing"
check_answer_prompt: ""
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"
check_answer_prompt: ""
hint_text: "Amount paid before any charges"
question_text: "What is the basic monthly rent after staircasing?"
leaseholdcharges: leaseholdcharges:
page_header: "" page_header: ""
has_mscharge: has_mscharge:

6
config/locales/validations/sales/sale_information.en.yml

@ -19,6 +19,8 @@ en:
exdate: exdate:
must_be_before_saledate: "Contract exchange date must be before sale completion date." 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." 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: fromprop:
previous_property_type_bedsit: "A bedsit cannot have more than 1 bedroom." previous_property_type_bedsit: "A bedsit cannot have more than 1 bedroom."
frombeds: frombeds:
@ -125,3 +127,7 @@ en:
postcode_full: 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_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." 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."

11
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

5
db/schema.rb

@ -761,6 +761,11 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do
t.bigint "created_by_id" t.bigint "created_by_id"
t.integer "has_management_fee" t.integer "has_management_fee"
t.decimal "management_fee", precision: 10, scale: 2 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 ["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 ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"

4
spec/features/user_spec.rb

@ -61,7 +61,7 @@ RSpec.describe "User Features" do
context "when the user has forgotten their password" 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 it "is redirected to the reset password page when they click the reset password link" do
visit("/lettings-logs") visit("/lettings-logs")
click_link("reset your password") click_link("Forgot password")
expect(page).to have_current_path("/account/password/new") expect(page).to have_current_path("/account/password/new")
end 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 it "is redirected to the reset password page when they click the reset password link" do
visit("/account/sign-in") visit("/account/sign-in")
click_link("reset your password") click_link("Forgot password")
expect(page).to have_current_path("/account/password/new") expect(page).to have_current_path("/account/password/new")
end end

4
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 describe "questions" do
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) } 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 context "when 2022" do
let(:start_date) { Time.utc(2022, 2, 8) } let(:start_date) { Time.utc(2022, 2, 8) }

4
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]) expect(page.questions.map(&:id)).to eq(%w[equity])
end end
it "has the correct id" do
expect(page.id).to eq("equity")
end
it "has the correct description" do it "has the correct description" do
expect(page.description).to be_nil expect(page.description).to be_nil
end end

31
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

31
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

31
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

31
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

31
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

38
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

2
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 RSpec.describe Form::Sales::Pages::ValueSharedOwnership, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil } let(:page_id) { "value_shared_ownership" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) } let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) }

2
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_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } let(: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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)

37
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

37
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

84
spec/models/form/sales/questions/mortgageused_spec.rb

@ -3,16 +3,24 @@ require "rails_helper"
RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch:) } subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch:) }
let(:ownershipsch) { 1 }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { 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(:stairowned) { nil }
let(:staircase) { nil } let(:staircase) { nil }
let(:saledate) { Time.zone.today } let(:saledate) { Time.zone.today }
let(:log) { build(:sales_log, :in_progress, ownershipsch:, stairowned:, staircase:) } let(:log) { build(:sales_log, :in_progress, ownershipsch:, stairowned:, staircase:) }
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 it "has the correct answer_options" do
expect(question.answer_options).to eq({ expect(question.answer_options).to eq({
"1" => { "value" => "Yes" }, "1" => { "value" => "Yes" },
@ -21,10 +29,13 @@ RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do
}) })
end end
describe "the displayed answer options" do
context "when it is a discounted ownership sale" do context "when it is a discounted ownership sale" do
let(:ownershipsch) { 2 } 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 it "does not show the don't know option" do
expect_the_question_not_to_show_dont_know expect_the_question_not_to_show_dont_know
end end
@ -34,20 +45,14 @@ RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do
let(:ownershipsch) { 3 } let(:ownershipsch) { 3 }
context "and the saledate is before 24/25" do context "and the saledate is before 24/25" do
before do let(:saledate) { Time.zone.local(2023, 5, 1) }\
allow(form).to receive(:start_year_2024_or_later?).and_return false
end
it "does not show the don't know option" do it "does show the don't know option" do
expect_the_question_not_to_show_dont_know expect_the_question_to_show_dont_know
end
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 end
context "and the saledate is 24/25" do
it "shows the don't know option" do it "shows the don't know option" do
expect_the_question_to_show_dont_know expect_the_question_to_show_dont_know
end end
@ -87,6 +92,57 @@ RSpec.describe Form::Sales::Questions::Mortgageused, type: :model do
end end
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 private
def expect_the_question_not_to_show_dont_know def expect_the_question_not_to_show_dont_know

33
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

40
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

33
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

33
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

4
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(: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, 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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
end end

6
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_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) } let(: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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)

11
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(:section_definition) { nil }
let(:form) { instance_double(Form, start_year_2025_or_later?: false) } 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 it "has correct form" do
expect(sale_information.form).to eq(form) expect(sale_information.form).to eq(form)
end end
@ -22,11 +26,16 @@ RSpec.describe Form::Sales::Sections::SaleInformation, type: :model do
end end
context "when form is 2025 or later" do 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 it "has correct subsections" do
expect(sale_information.subsections.map(&:id)).to eq(%w[ expect(sale_information.subsections.map(&:id)).to eq(%w[
shared_ownership_initial_purchase shared_ownership_initial_purchase
shared_ownership_staircasing_transaction
discounted_ownership_scheme discounted_ownership_scheme
outright_sale outright_sale
]) ])

2
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 shared_ownership_previous_tenure
value_shared_ownership value_shared_ownership
about_price_shared_ownership_value_check about_price_shared_ownership_value_check
equity initial_equity
shared_ownership_equity_value_check shared_ownership_equity_value_check
mortgage_used_shared_ownership mortgage_used_shared_ownership
mortgage_used_mortgage_value_check mortgage_used_mortgage_value_check

30
spec/models/location_spec.rb

@ -740,13 +740,41 @@ RSpec.describe Location, type: :model do
describe "#units" do describe "#units" do
let(:location) { FactoryBot.build(:location) } let(:location) { FactoryBot.build(:location) }
it "does add an error when the number of units is invalid" do context "when the number of units is invalid" do
it "adds an error when units is nil" do
location.units = nil location.units = nil
location.valid?(:units) 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) expect(location.errors.count).to eq(1)
end end
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
describe "#type_of_unit" do describe "#type_of_unit" do
let(:location) { FactoryBot.build(:location) } let(:location) { FactoryBot.build(:location) }

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

@ -1407,4 +1407,44 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
end 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 end

2
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.") .to include("This location is incomplete. Select another location or update this one.")
end end
it "produces no error when location is completes" do it "produces no error when location is complete" do
location.update!(units: 1) location.update!(units: 1)
location.reload location.reload
record.location = location record.location = location

11
spec/requests/form_controller_spec.rb

@ -582,8 +582,9 @@ RSpec.describe FormController, type: :request do
allow(Rails.logger).to receive(:info) allow(Rails.logger).to receive(:info)
end 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 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("There is a problem")
expect(page).to have_content("Error: What is the tenant’s age?") expect(page).to have_content("Error: What is the tenant’s age?")
end end
@ -591,6 +592,8 @@ RSpec.describe FormController, type: :request do
it "resets errors when fixed" 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: params
post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: valid_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}" get "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}"
expect(page).not_to have_content("There is a problem") expect(page).not_to have_content("There is a problem")
end end
@ -616,6 +619,7 @@ RSpec.describe FormController, type: :request do
it "validates the date correctly" do it "validates the date correctly" do
post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params 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("There is a problem")
end end
end end
@ -693,6 +697,7 @@ RSpec.describe FormController, type: :request do
it "validates the date correctly" do it "validates the date correctly" do
post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params: params 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("There is a problem")
end end
end end
@ -713,6 +718,7 @@ RSpec.describe FormController, type: :request do
it "validates the date correctly" do it "validates the date correctly" do
post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params: params post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params: params
follow_redirect!
expect(page).to have_content("There is a problem") expect(page).to have_content("There is a problem")
end end
end end
@ -748,8 +754,9 @@ RSpec.describe FormController, type: :request do
Timecop.unfreeze Timecop.unfreeze
end 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 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("There is a problem")
expect(page).to have_content("Error: Which organisation manages this letting?") expect(page).to have_content("Error: Which organisation manages this letting?")
end end

Loading…
Cancel
Save